The TCP connection close is a surprisingly complex dance of packets, designed to ensure no data gets lost in transit.
Let’s watch it happen. Imagine client.example.com wants to close a connection to server.example.com.
Client -> Server: FIN (10:30:01.123)
This FIN (Finish) packet is the client saying, "I’m done sending data, but I might still receive data from you." The server, upon receiving this, immediately acknowledges it:
Server -> Client: ACK (10:30:01.124)
Now, the server is in a state called CLOSE_WAIT. It knows the client is done, but it might still have data it needs to send. So, it continues to process any incoming data from the client and sends out any buffered data it still has. Once the server has sent all its data and is also ready to close, it sends its own FIN:
Server -> Client: FIN (10:30:05.456)
This FIN from the server is its "Okay, I’m also done sending data." The client, upon receiving this, sends back a final acknowledgment:
Client -> Server: ACK (10:30:05.457)
This final ACK completes the four-way handshake. The connection is now officially closed. The client enters a TIME_WAIT state for a period (typically 2 * MSL, Maximum Segment Lifetime, about 60 seconds) to ensure the final ACK isn’t lost and to handle any stray packets that might arrive.
The problem this solves is ensuring that both sides of a TCP connection agree on when the connection is truly finished and that all in-flight data has been successfully delivered and acknowledged. It’s a robust way to prevent data loss during shutdown.
The key to understanding this is recognizing the asymmetric nature of the FIN packet. A FIN signals the end of sending data, not necessarily the end of receiving data. This is why the server can be in CLOSE_WAIT for a while: it’s received the client’s signal to stop sending, but it still has its own buffered data to push out.
The actual levers you control are often at the application level, through socket options. For instance, SO_LINGER can modify this behavior. If SO_LINGER is set with a timeout of 0, a SO_CLOSE call will send a RST (Reset) instead of a FIN, abruptly terminating the connection and potentially discarding unsent data. If it’s set with a non-zero timeout, the application will attempt to send remaining data and then close gracefully, but if the timeout expires before the data is sent, it will then send a RST.
Most applications, however, rely on the default SO_LINGER behavior (which is typically a zero timeout, but the socket implementation might have its own grace period before sending the RST if data is still pending). This default behavior is usually sufficient for most use cases, allowing the application to signal closure and the TCP stack to manage the graceful shutdown.
The most surprising thing is how often this mechanism is bypassed or misunderstood, leading to connections that appear closed to one side but are still technically open and consuming resources on the other, especially in distributed systems where network partitions can interrupt the final ACK or FIN.
The next concept you’ll likely encounter is the RST packet and its role in abrupt connection termination.