Skip to main content

The TCP Handshake & Connection Lifecycle

TCP is a connection-oriented protocol: before any data moves, both sides agree to talk. That agreement is the three-way handshake. When they are done, a four-way exchange tears the connection down. Each side is a small state machine, and every segment on the wire pushes it forward.

Step through the whole lifecycle:

ESTABLISHEDCLOSEDsegment in flight
Client
CLOSED
Server
LISTEN
Connection closed. Step through the lifecycle. (phase: idle)
step 0 / 10

Opening: the three-way handshake

  1. SYN - the client picks a random initial sequence number and sends a segment with the SYN flag set. It moves to SYN_SENT.
  2. SYN-ACK - the server acknowledges the client's SYN and sends its own SYN. It moves to SYN_RCVD.
  3. ACK - the client acknowledges the server's SYN. Both sides are now ESTABLISHED and data can flow.

Three messages, not two, because sequence numbers travel in both directions and each direction must be acknowledged.

Worked example: the sequence numbers

Say the client picks a random ISN (initial sequence number) of 1000 and the server picks 5000. Walk the three segments:

  1. SYN: client -> server, seq=1000. No data yet, but SYN itself consumes one sequence number.
  2. SYN-ACK: server -> client, seq=5000, ack=1001. The ack=1001 says "I received your byte 1000 (the SYN) and expect 1001 next." The server's own SYN also consumes a sequence number.
  3. ACK: client -> server, seq=1001, ack=5001. The client acknowledges the server's SYN the same way.

From here the first real data byte the client sends carries seq=1001, and the server's first data byte carries seq=5001. Every subsequent segment's ack field is simply "highest contiguous byte received + 1," which is why a receiver can detect a gap: if it has acked up through 2000 and a segment arrives with seq=2500, the 500 bytes in between are missing and it keeps re-acking 2000 until they show up.

Closing: the four-way teardown

Closing takes four segments because each direction is shut independently. The client sends FIN, the server acks it, the server sends its own FIN, and the client acks that.

Why TIME_WAIT

After its final ACK the client does not close immediately - it waits 2·MSL (twice the maximum segment lifetime) in TIME_WAIT. This lets any delayed duplicate segments drain from the network and guarantees the server's FIN was actually acknowledged. Only then does the connection fully close.

TIME_WAIT exhaustion in production

Whichever side sends the first FIN is the one that parks in TIME_WAIT - and with MSL commonly set to 60-120 seconds by the OS, that is 2-4 minutes per connection. A busy server or load balancer that opens a fresh outbound connection per request (instead of reusing a connection pool) can pile up tens of thousands of TIME_WAIT sockets, each holding a local (IP, ephemeral port) pair. Once the ephemeral port range (often ~28,000 ports) is exhausted, new outbound connections fail with EADDRNOTAVAIL even though the box is otherwise idle. This is why HTTP keep-alive and connection pooling matter so much for high-throughput services, and why servers often initiate the close from the side that will not accumulate TIME_WAIT (some proxies deliberately let the client close first) or enable SO_REUSEADDR/net.ipv4.tcp_tw_reuse to recycle TIME_WAIT sockets sooner.