TCP Connection Teardown

Intuition

Opening a TCP connection takes three messages; closing it takes four. The reason is asymmetry. A connection is really two one-way pipes — Client→Server and Server→Client — and TCP closes them independently. When your side is done sending, you send a FIN ("I have no more data"). But the other side might not be done yet, so it acknowledges your FIN right away and only sends its own FIN later, once it has finished. That's why the middle ACK and the peer's FIN are two separate segments instead of one combined SYN-ACK-style message: a close has to leave room for a half-open connection where one pipe is shut and the other is still flowing.

How It Works

One peer is the active closer (here, the Client — its application called close() first); the other is the passive closer (the Server). Each runs its own little state machine, and the four segments drive the transitions.

The active closer walks ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED. The passive closer walks ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED. Reading those two ladders top to bottom is the protocol — the animation drops each state beside its lifeline at the moment it changes, so the full sequence builds up as you step through it.

The subtle state is the active closer's last one, TIME_WAIT. After sending the final ACK, the active closer does not close immediately. It waits 2·MSL (twice the maximum segment lifetime). This linger does two jobs: if the final ACK was lost, the passive closer will retransmit its FIN, and the active closer must still be alive to re-acknowledge it; and it lets any stray duplicate segments from this connection die out before the same socket pair could be reused. Switch the scenario selector to Lost final ACK to watch exactly this — the ACK drops, the Server resends its FIN, and the lingering Client re-ACKs from TIME_WAIT.

Step By Step

Walking the default Normal close scenario:

  1. ESTABLISHED. Both peers have an open connection; the Client's app calls close() first.
  2. FIN → (segment 1). The Client sends a FIN with seq = u and moves to FIN_WAIT_1. It is saying "I have no more data."
  3. Server receives the FIN. The Server learns the Client is done sending and moves to CLOSE_WAIT.
  4. ACK ← (segment 2). The Server acknowledges with ack = u+1.
  5. Client receives the ACK. The Client moves to FIN_WAIT_2. The Client→Server pipe is now closed.
  6. Half-closed. Here is the whole reason for four segments: the Server may still have buffered data to send, so its ACK (already sent) and its FIN (not yet) are separate events. The Server→Client pipe stays open.
  7. FIN ← (segment 3). The Server finishes sending, then sends its own FIN with seq = w, ack = u+1, and moves to LAST_ACK.
  8. Client receives the FIN. The Client moves to TIME_WAIT.
  9. ACK → (segment 4). The Client sends the final ACK with ack = w+1.
  10. Server receives the ACK. The Server moves to CLOSED — it is fully done.
  11. TIME_WAIT. The Client lingers for 2·MSL, ready to re-ACK a retransmitted FIN and letting old duplicates expire.
  12. CLOSED. The timer elapses and the Client closes. The connection is gone.

Watch the segment counter in the top-right tick from 1/4 to 4/4 as each FIN/ACK crosses the channel, and watch the two state ladders fill in — the Server reaches CLOSED one beat before the Client, which is still serving out its TIME_WAIT.

Complexity

QuantityValue
Control segments4 (FIN, ACK, FIN, ACK)
Active-closer states4 transitions (FIN_WAIT_1, FIN_WAIT_2, TIME_WAIT, CLOSED)
Passive-closer states3 transitions (CLOSE_WAIT, LAST_ACK, CLOSED)
TIME_WAIT duration2·MSL (typically 1–4 minutes)

The animated version runs a fixed, lossless exchange in the Normal scenario; the Lost final ACK scenario adds one retransmission round to show the TIME_WAIT rationale.

Edge Cases

  • Lost final ACK. The passive closer's retransmission timer fires and it resends its FIN; the active closer re-ACKs from TIME_WAIT and restarts the timer. The selectable scenario.
  • Simultaneous close. If both sides call close() at once, each goes FIN_WAIT_1 → CLOSING → TIME_WAIT. (Not animated here.)
  • FIN_WAIT_2 timeout. If the passive side never sends its FIN, the active side cannot wait forever; real stacks cap FIN_WAIT_2.
  • RST instead of FIN. An abortive close sends a reset, skipping the graceful four-way exchange entirely.

Common Mistakes

  • Thinking the ACK and second FIN must combine into three segments. They can't in general, because the passive side may still be sending data — that's the half-close window.
  • Believing both sides reach CLOSED together. The passive closer reaches CLOSED as soon as it receives the final ACK; the active closer lingers in TIME_WAIT for 2·MSL first.
  • Assuming TIME_WAIT is wasted time. It is what makes the close robust: without it, a lost final ACK would leave the peer stuck and a reused socket could swallow a stale duplicate.
  • Putting TIME_WAIT on the wrong side. It belongs to the active closer (the side that sent the first FIN), not the passive one.

A Note on Simplification

This is an explainer, not a substitute for the RFCs. It uses symbolic sequence numbers (u, w) rather than real byte counts, carries no data bytes, and treats the retransmission timer and 2·MSL as idealized. It also omits several real mechanisms: abortive RST closes, simultaneous close (the CLOSING state), the FIN_WAIT_2 timeout, delayed-ACK and Nagle interactions, and the SO_REUSEADDR / TIME_WAIT reuse details that matter to servers under load. The goal is the shape of the four-way handshake and why TIME_WAIT exists — consult RFC 793 / RFC 9293 for the full state machine.