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:
Opening: the three-way handshake
- SYN - the client picks a random initial sequence number and sends a
segment with the SYN flag set. It moves to
SYN_SENT. - SYN-ACK - the server acknowledges the client's SYN and sends its own SYN.
It moves to
SYN_RCVD. - ACK - the client acknowledges the server's SYN. Both sides are now
ESTABLISHEDand 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:
- SYN: client -> server,
seq=1000. No data yet, but SYN itself consumes one sequence number. - SYN-ACK: server -> client,
seq=5000, ack=1001. Theack=1001says "I received your byte 1000 (the SYN) and expect 1001 next." The server's own SYN also consumes a sequence number. - 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.
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.