26. The TCP finite state machine
26.1 The states
| State | Meaning |
|---|---|
CLOSED |
No connection. |
LISTEN |
Server waiting for SYNs. |
SYN_SENT |
Client sent SYN, waiting for SYN-ACK. |
SYN_RECV |
Server got SYN, sent SYN-ACK, waiting for final ACK (this is the SYN-queue state). |
ESTABLISHED |
Data transfer. |
FIN_WAIT_1 |
We sent FIN, waiting for its ACK (or peer's FIN). |
FIN_WAIT_2 |
Our FIN ACKed; waiting for peer's FIN. (tcp_fin_timeout bounds this ~60 s to avoid leaks against a peer that never closes.) |
CLOSING |
Both sent FIN ~simultaneously; waiting for ACK of ours. |
TIME_WAIT |
We sent the last ACK; wait 2×MSL. |
CLOSE_WAIT |
Peer sent FIN, we ACKed it; waiting for our app to call close(). |
LAST_ACK |
We (after CLOSE_WAIT) sent our FIN; waiting for its ACK. |
26.2 The diagram (active vs passive paths)
CLOSED
active open | \ passive open
send SYN | \ (listen())
v v
rcv SYN,ACK SYN_SENT LISTEN
send ACK +-----------| | rcv SYN
| | (simultaneous| send SYN,ACK
| open path) v
| SYN_RECV
| rcv ACK / |
v / |
ESTABLISHED <-----------+ |
______/ | \______
close()/send FIN | rcv FIN / send ACK
v | v
FIN_WAIT_1 | CLOSE_WAIT
| | rcv ACK | | close()/send FIN
| v | v
| FIN_WAIT_2 | LAST_ACK
| | rcv FIN | | rcv ACK
rcv FIN | send ACK| v
send ACK| | CLOSED
v v
CLOSING -> (rcv ACK) -> TIME_WAIT -> (2MSL) -> CLOSED
26.3 How to read states operationally
- Many
SYN_RECVon a server = SYN flood or a client population that sends SYN but not the final ACK (broken NAT, PMTU black hole on the SYN-ACK). - Many
SYN_SENTon a client = destination unreachable / firewalled / overloaded. FIN_WAIT_2piling up = the peer isn't closing its side (peer app bug or peer overload).tcp_fin_timeoutwill eventually reap them.CLOSE_WAITpiling up = local app bug (didn'tclose()), §25.5.LAST_ACKstuck = the final ACK from the peer isn't arriving (loss, peer gone).TIME_WAITlarge but stable = normal for a busy connection initiator; only act if you're near port exhaustion.
26.4 The kernel timers attached to a connection
| Timer | Fires when | Effect |
|---|---|---|
| Retransmission (RTO) | an unACKed segment's timer expires | resend oldest unacked segment, back off RTO, cut congestion window (Ch. 28, 31) |
| Persist | receiver advertised a zero window | send tiny "window probes" so a lost window-update doesn't deadlock the connection (Ch. 30) |
| Keepalive | connection idle for tcp_keepalive_time (default 7200 s) |
send a probe; after tcp_keepalive_probes failures, kill the connection (Ch. 34) |
| TIME_WAIT (2MSL) | last ACK sent | reap the TCB |
| FIN_WAIT_2 | our FIN ACKed but no peer FIN | reap after tcp_fin_timeout |
| Delayed ACK | data received, nothing to piggyback on | send a bare ACK after up to ~40–200 ms (Ch. 33) |
26.5 War story
A batch job's worker connections sat in ESTABLISHED forever after the remote
database VM was hard-killed (no FIN, no RST). The app blocked on read()
indefinitely; the job hung for hours. Default keepalive is 2 hours idle +
9 probes × 75 s ≈ 2h11m before the kernel gives up — far too slow. Setting
TCP_KEEPIDLE=30, TCP_KEEPINTVL=10, TCP_KEEPCNT=3 on the socket (or an
application-level read deadline) made it fail in ~1 minute and retry.
Lesson: the OS will not promptly tell you a peer vanished; set per-socket
keepalive or app-level timeouts for anything long-lived.
Try it yourself
ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn # histogram of TCP states
watch -n1 'ss -tan state syn-recv | wc -l'
sysctl net.ipv4.tcp_keepalive_time net.ipv4.tcp_keepalive_intvl net.ipv4.tcp_keepalive_probes
ss -tie # per-socket: state, timers, retrans count