TCP State Transition Diagram

 

TCP State Transition Diagram
Chapter 61 · Part 2 of 4 · EmbeddedPathashala

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(), or close()
  • 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:

Event label (what triggered the move)

Either an application call like close() shown in bold, or a received segment like recv: SYN.

Send label (what TCP sends during the transition)

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

Current State Event (Trigger) Segment Sent Next State
CLOSED App: passive open (listen()) none LISTEN
CLOSED App: active open (connect()) SYN SYN_SENT
LISTEN recv: SYN from client SYN,ACK SYN_RECV
SYN_SENT recv: SYN,ACK from server ACK ESTABLISHED
SYN_RECV recv: ACK from client none ESTABLISHED
ESTABLISHED App: close() (active close) FIN FIN_WAIT1
ESTABLISHED recv: FIN from peer (passive close) ACK CLOSE_WAIT
FIN_WAIT1 recv: ACK from peer none FIN_WAIT2
FIN_WAIT1 recv: FIN from peer (simultaneous close) ACK CLOSING
FIN_WAIT1 recv: FIN,ACK from peer ACK TIME_WAIT
FIN_WAIT2 recv: FIN from peer ACK TIME_WAIT
CLOSING recv: ACK from peer none TIME_WAIT
TIME_WAIT 2×MSL timeout expires none CLOSED
CLOSE_WAIT App: close() FIN LAST_ACK
LAST_ACK recv: ACK from peer none CLOSED

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).

CLOSED
App: connect() → sends SYN
SYN_SENT
recv: SYN-ACK → sends ACK
ESTABLISHED
← Data transfer happens here →
App: close() → sends FIN
FIN_WAIT1
recv: ACK → sends nothing
FIN_WAIT2
recv: FIN → sends ACK
TIME_WAIT
2×MSL timeout (120s) expires
CLOSED

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.

CLOSED
App: listen() → passive open
LISTEN
recv: SYN → sends SYN-ACK
SYN_RECV
recv: ACK → sends nothing
ESTABLISHED
← Data transfer happens here →
recv: FIN from client → sends ACK
CLOSE_WAIT
App: close() → sends FIN
LAST_ACK
recv: ACK → connection done
CLOSED

The Rare Case: Simultaneous Close

What is a 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.

Side A
ESTABLISHED
App: close() → send FIN
FIN_WAIT1
recv: FIN (not ACK!) → send ACK
CLOSING
recv: ACK
TIME_WAIT → CLOSED
Side B
ESTABLISHED
App: close() → send FIN
FIN_WAIT1
recv: FIN (not ACK!) → send ACK
CLOSING
recv: ACK
TIME_WAIT → CLOSED

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

Q1. What are the two types of events that cause TCP state transitions?

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.

Q2. Why does only the active closer go through TIME_WAIT in a normal close?

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.

Q3. What happens if a TCP in FIN_WAIT1 receives a FIN instead of an ACK?

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.

Q4. Can a server ever enter TIME_WAIT state? Explain.

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.

Q5. How does the Linux kernel handle the SYN_RECV state internally? What are the two queues involved?

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.

Leave a Reply

Your email address will not be published. Required fields are marked *