State machine
TCP is described by a state machine with 11 states (RFC 793). Each connection on each side is in exactly one of them at any moment. Transitions happen when SYN/ACK/FIN/RST segments are sent or received, or when timers fire.
Client and server sides
Server (passive open) Client (active open)
CLOSED CLOSED
│ │
│ listen() │ connect()
▼ ▼
LISTEN SYN_SENT
│ │
│ rcv SYN, send SYN-ACK │ rcv SYN-ACK, send ACK
▼ ▼
SYN_RECV ESTABLISHED
│
│ rcv ACK
▼
ESTABLISHED
Key states
- LISTEN - the server socket is waiting for a SYN. Visible in
ss -tln - SYN_SENT - the client sent a SYN and is waiting for a reply
- SYN_RECV - the server received a SYN, sent a SYN-ACK, and is waiting for the ACK
- ESTABLISHED - handshake complete; data can flow
- FIN_WAIT_1 - we initiated the close (sent FIN)
- FIN_WAIT_2 - the peer ACKed our FIN; we are waiting for the peer's FIN
- CLOSE_WAIT - the peer sent FIN first and we ACKed it; your job now is to call close(). If the application does not close the socket, it stays here
- LAST_ACK - we sent our FIN in response to CLOSE_WAIT and are waiting for the ACK
- TIME_WAIT - both sides sent FIN+ACK; we wait 2*MSL (~60 s) to absorb any delayed packets from the old connection
- CLOSED - no connection
TIME_WAIT: why you see so many
After a close, the active side (the one that closed first) stays in TIME_WAIT for ~60 seconds. This is by design: it protects against packets from an old connection arriving at a new one that reuses the same 4-tuple (src/dst IP+port).
On a client that opens many short-lived connections (HTTP/1.0 without keep-alive, redis clients without pooling), outgoing ports run out fast:
ss -tn state time-wait | wc -l
# thousands means potential source-port exhaustion
Solutions:
- HTTP keep-alive or connection pooling in the application
net.ipv4.tcp_tw_reuse=1to reuse TIME_WAIT sockets for outgoing connect (on the client)net.ipv4.ip_local_port_rangeto widen the ephemeral port range
CLOSE_WAIT: the application forgot close()
CLOSE_WAIT means the peer closed its side and the kernel is waiting for you
to call close(). If the count grows over time, the application has a leak:
it receives EOF on a socket and does not close the file descriptor. This is
a classic bug in EOF handling in Node.js, Java, and Python.
Visible via ss -tn state close-wait.