Closing a TCP connection is a second handshake — four packets, an asymmetric state dance, and a two-minute ghost that every server operator has met under the name TIME_WAIT. Here is what the kernel actually does when your application calls close().
The application calls close(fd) (or the socket is closed implicitly at process exit). The kernel transitions the socket to FIN_WAIT_1 and transmits a FIN segment — an empty segment with the FIN flag, consuming one sequence number. Data sent before the close is still delivered; FIN is just a queued marker after the last byte. If the send buffer isn't empty, the FIN waits behind it.
The peer's kernel receives the FIN, ACKs it, and signals the application via read() returning 0 (EOF). The peer's socket enters CLOSE_WAIT. Critically, the peer can keep sending: the connection is half-closed. This asymmetry is what makes shutdown(SHUT_WR) useful — the local side declares "I'm done sending" while still reading. When the peer's application eventually calls close(), its kernel sends a FIN back.
The initiating side receives the peer's FIN and ACKs it, then enters TIME_WAIT — for 60 seconds on Linux (2 × MSL, where MSL is the 30-second max segment lifetime). The socket is now a ghost: no data flows, no bytes transfer, but the 4-tuple (src IP, src port, dst IP, dst port) is reserved. The peer, having received the final ACK, goes straight to CLOSED.
TIME_WAIT's job: the final ACK can be lost, and the peer's retransmitted FIN needs an ACK to land in. It also guarantees no stale segments from this connection — delayed by network weirdness for up to 2MSL — can be mistaken for data in a new connection reusing the same 4-tuple. After 60 seconds the socket is destroyed and the port is reusable. This is the mechanism behind the classic TIME_WAIT pile-up on high-churn servers: hundreds of thousands of ghost sockets, each holding a port hostage for a minute.
ss -tan state time-wait | wc -lThe abrupt alternative: an RST segment. RST is sent when a host receives a packet for a connection it doesn't know (a SYN to a closed port, or a packet on a socket already destroyed) or when an application aborts via SO_LINGER with a zero timeout. An RST is answered with nothing — the connection dies instantly on both sides, no TIME_WAIT, no graceful exchange. Connection reset by peer is the user-visible scar.
sudo tcpdump -ni any 'tcp and (tcp[tcpflags] & (tcp-fin|tcp-rst) != 0)'What the machine actually does on close is a small, careful funeral: mark, notify, wait, confirm, and then wait one more minute to be sure nothing dead comes back. The asymmetry — one side's ghost (TIME_WAIT) vs the other's clean exit — is the kernel erring on the side of correctness at the price of port pressure.