Skip to content

TCP State Machine

The TCP connection state machine (defined in RFC 793, with updates in RFC 1122) is one of the most Precisely specified protocol behaviors in all of networking. Every TCP endpoint transitions through Defined states as connections are established, used, and torn down. Understanding these states is Essential for troubleshooting connection issues, writing robust network software, and operating High-performance servers.

This document maps every state, every transition, every edge case (simultaneous open, simultaneous Close, half-open connections), and provides practical guidance for diagnosing TCP state problems in Production.

TCP defines 11 states. An endpoint is always in exactly one of these states:

StateDescription
CLOSEDNo connection. Initial state.
LISTENWaiting for connection requests.
SYN_SENTSent SYN, waiting for response.
SYN_RCVDReceived SYN, sent SYN+ACK, waiting for ACK.
ESTABLISHEDConnection open. Data transfer in progress.
FIN_WAIT_1Initiated close, sent FIN, waiting for ACK or FIN.
FIN_WAIT_2Received ACK for our FIN, waiting for peer”s FIN.
CLOSE_WAITReceived peer’s FIN, waiting for local close.
LAST_ACKSent our FIN after peer closed, waiting for ACK.
TIME_WAITWaited for peer’s ACK after our close. 2*MSL timer.
CLOSINGBoth sides sent FIN simultaneously, waiting for ACK.
stateDiagram-v2
    [*] --> CLOSED
    CLOSED --> LISTEN : passive_open
    CLOSED --> SYN_SENT : send_SYN
    LISTEN --> SYN_RCVD : recv_SYN, send_SYN+ACK
    SYN_SENT --> ESTABLISHED : recv_SYN+ACK, send_ACK
    SYN_SENT --> SYN_RCVD : recv_SYN, send_SYN+ACK
    SYN_RCVD --> ESTABLISHED : recv_ACK
    LISTEN --> SYN_SENT : send_SYN
    ESTABLISHED --> FIN_WAIT_1 : close, send_FIN
    ESTABLISHED --> CLOSE_WAIT : recv_FIN, send_ACK
    FIN_WAIT_1 --> FIN_WAIT_2 : recv_ACK
    FIN_WAIT_1 --> CLOSING : recv_FIN, send_ACK
    FIN_WAIT_1 --> TIME_WAIT : recv_FIN+ACK, send_ACK
    FIN_WAIT_2 --> TIME_WAIT : recv_FIN, send_ACK
    CLOSE_WAIT --> LAST_ACK : close, send_FIN
    LAST_ACK --> CLOSED : recv_ACK
    CLOSING --> TIME_WAIT : recv_ACK
    TIME_WAIT --> CLOSED : 2*MSL timeout

The three-way handshake establishes both ends of the connection and synchronizes sequence numbers. Both sides must agree on initial sequence numbers (ISNs) before any data can be exchanged.

State: CLOSED
|
|--- SYN (seq=1000) ---------------->|
State: SYN_SENT
|
|<-- SYN+ACK (seq=2000, ack=1001) ---|
State: ESTABLISHED (after sending ACK)
|
|--- ACK (ack=2001) ---------------->|
State: ESTABLISHED
State: LISTEN
|
|<-- SYN (seq=1000) -----------------|
State: SYN_RCVD
|
|--- SYN+ACK (seq=2000, ack=1001) -->|
|
|<-- ACK (ack=2001) -----------------|
State: ESTABLISHED

A two-way handshake would be vulnerable to stale duplicate SYNs from old connections. Consider:

  1. Client sends SYN (seq=1000). Network delay causes it to arrive late.
  2. Client times out, sends new SYN (seq=3000). Server responds, connection established.
  3. Data exchanged, connection closed.
  4. The stale SYN (seq=1000) finally arrives. Server creates a half-open connection.
  5. Client receives SYN+ACK for a connection it never intended, sends RST.

The three-way handshake prevents this because the client must acknowledge the server’s ISN. The Stale SYN would trigger a SYN+ACK from the server, but the client’s ACK would not match, and the Client would send RST.

ISNs must be unpredictable to prevent TCP sequence prediction attacks (Blind Spoofing, RFC 6528). Modern implementations use a cryptographically strong PRNG seeded with a combination of:

  • Source and destination IP addresses and ports
  • A secret key (per-boot random)
  • A high-resolution timestamp

Linux uses a 64-bit counter that increments by 1 for each microsecond and by 64,000 for each new Connection, making ISN prediction computationally infeasible.

Terminal window
# View Linux ISN generation parameters
sysctl net.ipv4.tcp_timestamps
sysctl net.ipv4.tcp_syncookies

TCP uses a four-way (full-duplex) close because each direction of the connection must be closed Independently. This is called a “half-close” — one side can stop sending while still receiving.

State: ESTABLISHED
|
|--- FIN (seq=5000) ---------------->|
State: FIN_WAIT_1
|
|<-- ACK (ack=5001) -----------------|
State: FIN_WAIT_2
| (waiting for peer to close its side)
|
|<-- FIN (seq=3000) -----------------|
State: TIME_WAIT (after sending ACK)
|
|--- ACK (ack=3001) ---------------->|
State: TIME_WAIT (wait 2*MSL)
State: ESTABLISHED
|
|<-- FIN (seq=5000) -----------------|
State: CLOSE_WAIT
| (application can still send data)
|
|--- ACK (ack=5001) ---------------->|
State: CLOSE_WAIT (still, waiting for app to close)
|
|--- FIN (seq=3000) ---------------->|
State: LAST_ACK
|
|<-- ACK (ack=3001) -----------------|
State: CLOSED

FIN_WAIT_2 is the state where the active closer has finished sending but is waiting for the peer to Finish. If the peer never sends FIN (application crash, bug, network partition), the endpoint stays In FIN_WAIT_2 indefinitely.

Linux protects against this with tcp_fin_timeout (default 60 seconds). After this timeout, the Kernel forcibly closes the connection and frees the resources.

Terminal window
# View and adjust FIN_WAIT_2 timeout
sysctl net.ipv4.tcp_fin_timeout
# Count connections in FIN_WAIT_2 state
ss -tan state fin-wait-2 | wc -l