Understand how TCP moves between states — what triggers each transition, and how client and server paths differ.
State Transitions — The Big Picture
We know all the TCP states from Part 1. But TCP does not jump between states randomly — each move from one state to another is triggered by a specific event. An event is either:
- An action by the application — like calling
connect(),listen(), orclose() - A segment arriving from the network — like receiving a SYN, a SYN-ACK, a FIN, or an ACK
When a transition happens, TCP often sends a segment back to the peer. For example: when a server in LISTEN state receives a SYN, it sends back a SYN-ACK.
In this tutorial, we walk through the two main paths through the state machine — the client path (active open) and the server path (passive open) — and explain each transition step by step.
How to Read a State Transition Diagram
Each arrow in a TCP state diagram has two labels:
Either an application call like close() shown in bold, or a received segment like recv: SYN.
Shown as send: SYN,ACK or send: <nil> if nothing is sent.
Example: The arrow from LISTEN to SYN_RECV says:
recv: SYN → send: SYN,ACK
which means: “when in LISTEN state and we receive a SYN, move to SYN_RECV and send a SYN-ACK”.
Complete State Transition Table
Client Path — Active Open and Active Close
The typical client goes through these states in order. The client is the one that calls connect() (active open) and usually the one that calls close() first (active close).
Server Path — Passive Open and Passive Close
The server calls listen() (passive open) and usually closes second (passive close). The server path through the state machine is the mirror image of the client path.
The Rare Case: Simultaneous Close
Normally one side closes first and the other closes second. But what if both sides call close() at exactly the same time? Both send FIN segments simultaneously. Each side is in FIN_WAIT1 (expecting an ACK) but instead receives a FIN. This causes both sides to enter the CLOSING state.
Notice that in a simultaneous close, both sides end up in TIME_WAIT, unlike a normal close where only the active closer goes through TIME_WAIT.
Observing State Transitions with ss / netstat
You can watch TCP state transitions in real time using the ss command:
/* Watch all TCP sockets, refresh every second */
$ watch -n 1 'ss -tan'
/* Filter by state */
$ ss -tan state established # Only ESTABLISHED
$ ss -tan state time-wait # Only TIME_WAIT
$ ss -tan state close-wait # Only CLOSE_WAIT (bug detector)
/* Count sockets per state */
$ ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn
3842 TIME-WAIT
156 ESTABLISHED
4 CLOSE-WAIT ← potential bug if this keeps growing
1 LISTEN
A one-time snapshot script to log TCP states:
#!/bin/bash
# tcp_state_snapshot.sh
# Prints a count of sockets in each TCP state
echo "=== TCP Socket State Snapshot ==="
echo "Time: $(date)"
echo ""
ss -tan | awk 'NR>1 {states[$1]++}
END {
for (s in states)
printf "%-15s %d\n", s, states[s]
}' | sort -k2 -rn
Interview Questions
Answer: TCP state transitions are triggered by (1) application actions — system calls like connect(), listen(), accept(), close(), or send/recv operations — and (2) network events — receipt of a segment from the peer TCP, such as a SYN, SYN-ACK, FIN, or ACK segment. During a transition, TCP may also send a segment back to the peer.
Answer: The active closer is the side that sends the last ACK (acknowledging the passive closer’s FIN). Since this last ACK might be lost, the active closer needs to stay alive in TIME_WAIT to retransmit it if the passive closer resends its FIN. The passive closer, after sending its FIN, moves from LAST_ACK directly to CLOSED when it receives the ACK, since it does not need to send anything further.
Answer: This is a simultaneous close scenario. Both sides called close() at the same time. The TCP in FIN_WAIT1 was expecting an ACK for its own FIN, but instead received a FIN from the peer (meaning the peer also closed). In this case, the TCP sends an ACK and moves to the CLOSING state rather than FIN_WAIT2. Both sides eventually end up in TIME_WAIT.
Answer: Yes. TIME_WAIT is entered by whoever performs the active close — the side that calls close() first. It is not exclusively a client-side state. If a server application calls close() before the client does (for example, an HTTP server that sends a response and immediately closes), then the server enters TIME_WAIT. This is common with short-lived HTTP/1.0 connections where the server terminates each connection after one request-response cycle.
Answer: Linux maintains two queues for each listening socket: (1) the SYN queue (incomplete connection queue) — holds connections in SYN_RECV state, where the server has received a SYN, sent SYN-ACK, but is waiting for the final ACK; (2) the accept queue (complete connection queue) — holds connections that have completed the three-way handshake (ESTABLISHED) but have not yet been retrieved by the application via accept(). The backlog parameter to listen() controls the size of the accept queue.
