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:
- ESTABLISHED. Both peers have an open connection; the Client's app calls
close()first. - FIN → (segment 1). The Client sends a
FINwithseq = uand moves toFIN_WAIT_1. It is saying "I have no more data." - Server receives the FIN. The Server learns the Client is done sending and
moves to
CLOSE_WAIT. - ACK ← (segment 2). The Server acknowledges with
ack = u+1. - Client receives the ACK. The Client moves to
FIN_WAIT_2. The Client→Server pipe is now closed. - 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 itsFIN(not yet) are separate events. The Server→Client pipe stays open. - FIN ← (segment 3). The Server finishes sending, then sends its own
FINwithseq = w, ack = u+1, and moves toLAST_ACK. - Client receives the FIN. The Client moves to
TIME_WAIT. - ACK → (segment 4). The Client sends the final
ACKwithack = w+1. - Server receives the ACK. The Server moves to
CLOSED— it is fully done. - TIME_WAIT. The Client lingers for
2·MSL, ready to re-ACK a retransmittedFINand letting old duplicates expire. - 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
| Quantity | Value |
|---|---|
| Control segments | 4 (FIN, ACK, FIN, ACK) |
| Active-closer states | 4 transitions (FIN_WAIT_1, FIN_WAIT_2, TIME_WAIT, CLOSED) |
| Passive-closer states | 3 transitions (CLOSE_WAIT, LAST_ACK, CLOSED) |
TIME_WAIT duration | 2·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 fromTIME_WAITand restarts the timer. The selectable scenario. - Simultaneous close. If both sides call
close()at once, each goesFIN_WAIT_1 → CLOSING → TIME_WAIT. (Not animated here.) FIN_WAIT_2timeout. If the passive side never sends itsFIN, the active side cannot wait forever; real stacks capFIN_WAIT_2.RSTinstead ofFIN. An abortive close sends a reset, skipping the graceful four-way exchange entirely.
Common Mistakes
- Thinking the
ACKand secondFINmust 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
CLOSEDtogether. The passive closer reachesCLOSEDas soon as it receives the finalACK; the active closer lingers inTIME_WAITfor2·MSLfirst. - Assuming
TIME_WAITis wasted time. It is what makes the close robust: without it, a lost finalACKwould leave the peer stuck and a reused socket could swallow a stale duplicate. - Putting
TIME_WAITon the wrong side. It belongs to the active closer (the side that sent the firstFIN), 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.