Every TCP connection on the internet begins the same way: with a three-way handshake between client and server. SYN, SYN-ACK, ACK. Three packets, one round trip, and the connection is established. Once you understand this dance, a lot of other things — connection latency, slow-start, half-open states, SYN flood attacks — start making sense.
This post walks through the TCP handshake in detail, what each step does, the TCP state machine that surrounds it, and how things go wrong.
The Handshake, Briefly
Client Server
│ ─── SYN (seq=x) ──────────────────────▶│
│ ◀─────────── SYN-ACK (seq=y, ack=x+1)──│
│ ─── ACK (ack=y+1) ────────────────────▶│
│ │
│ ◀─── Application data ───────────────▶│
Step 1: SYN
Client sends a SYN (synchronize) packet:
- Sequence number =
x(random initial value). - SYN flag set.
- No payload.
This says “I want to establish a connection. My initial sequence number is x.”
Step 2: SYN-ACK
Server responds with both SYN and ACK flags:
- Sequence number =
y(server’s own random initial value). - Acknowledgment number =
x + 1(acknowledging the client’s SYN). - SYN and ACK flags set.
- No payload.
“I accept the connection. I’m starting from sequence y. I acknowledged receiving your x.”
Step 3: ACK
Client confirms:
- Sequence number =
x + 1. - Acknowledgment number =
y + 1. - ACK flag set.
- May include payload (start of the data).
“Confirmed. We’re connected.”
After this, both sides can send data. The connection is established.
Why Three Steps
The handshake achieves a few things:
Mutual confirmation
Both sides confirm they can send and receive. SYN proves client can send; SYN-ACK proves server can send and received the SYN; ACK proves client received the SYN-ACK.
Sequence number agreement
TCP numbers every byte. Both sides need to agree on starting sequence numbers so they can detect lost, duplicate, or out-of-order packets.
Initial window negotiation
The handshake exchanges initial TCP options: window size, MSS (max segment size), window scaling, SACK support. These tune the connection for the path.
For MSS specifically, the values negotiated here determine packet sizing for the whole connection.
State Machine
Each side of a TCP connection moves through a state machine. Key states:
Client side
CLOSED→ (send SYN) →SYN-SENTSYN-SENT→ (receive SYN-ACK, send ACK) →ESTABLISHEDESTABLISHED→ (send FIN) →FIN-WAIT-1FIN-WAIT-1→ (receive ACK) →FIN-WAIT-2FIN-WAIT-2→ (receive FIN, send ACK) →TIME-WAITTIME-WAIT→ (after 2× MSL) →CLOSED
Server side
CLOSED→ (passive open, listen) →LISTENLISTEN→ (receive SYN, send SYN-ACK) →SYN-RECEIVEDSYN-RECEIVED→ (receive ACK) →ESTABLISHEDESTABLISHED→ (receive FIN, send ACK) →CLOSE-WAITCLOSE-WAIT→ (send FIN) →LAST-ACKLAST-ACK→ (receive ACK) →CLOSED
You see these states in netstat and ss output. Diagnosing TCP problems often starts with reading them.
Why TIME-WAIT Exists
After a connection closes, the client side stays in TIME-WAIT for ~2 minutes (2× Maximum Segment Lifetime). The purpose:
- Wait for delayed packets from the old connection to expire. Otherwise they might be mistaken for packets of a new connection on the same socket pair.
- Ensure the final ACK reaches the server. If lost, the server resends FIN; client must be there to ACK it.
Practical consequence: a busy server has many sockets in TIME-WAIT. The OS has a port pool; high churn can exhaust it. Modern Linux has settings (net.ipv4.tcp_tw_reuse) to recycle TIME-WAIT sockets safely.
The Slow-Start Era
After handshake, TCP doesn’t immediately send at full speed. It uses slow start — exponentially ramping up the send rate until packet loss occurs.
This means the first few KB of a TCP connection are slower than the steady-state. For a small request that fits in a few packets, slow start dominates the latency.
This is one of the reasons HTTP/2 multiplexing and HTTP/3 connection reuse matter — you avoid paying the slow-start cost on every request.
SYN Flood Attacks
A classic DDoS technique exploits the handshake:
- Attacker sends many SYN packets with spoofed source IPs.
- Server allocates state for each (in SYN-RECEIVED).
- Server sends SYN-ACK to the (spoofed) source addresses, getting no response.
- Server’s connection table fills up; legitimate clients can’t connect.
Mitigations:
- SYN cookies — server doesn’t allocate state until ACK arrives; encodes connection info in the SYN-ACK’s sequence number.
- Rate limiting at the firewall.
- DDoS protection services (Cloudflare, etc.) absorb floods.
RST: The Reset
The handshake can fail in various ways:
- Connection refused — Server isn’t listening on the port; sends RST in response to SYN.
- Mid-connection RST — Either side decides to abort; sends RST. Recipient sees connection ended abruptly.
- Stale connection RST — Server lost state (crashed and restarted); client’s existing connection’s packets get RST.
A clean close uses FIN. A RST is “abort immediately.”
Half-Open Connections
A connection where one side has closed but the other hasn’t. Often the result of a crash — one side disappears without sending FIN.
The remaining side doesn’t immediately know. It might:
- Wait for keepalive timeout.
- Discover when its next write fails.
- Stay open indefinitely if neither side talks.
This is why long-lived TCP connections need keepalive — otherwise they accumulate as zombies through network interruptions.
What This Looks Like in tcpdump
17:00:00.001 IP client.50001 > server.443: Flags [S], seq 1234567890, win 65535
17:00:00.012 IP server.443 > client.50001: Flags [S.], seq 987654321, ack 1234567891, win 32768
17:00:00.013 IP client.50001 > server.443: Flags [.], ack 987654322, win 65535
17:00:00.013 IP client.50001 > server.443: Flags [P.], seq 1234567891:1234568091, ack 987654322
S= SYNS.= SYN-ACK (S and ACK flags).= ACK onlyP.= ACK with PUSH (data)F.= FIN-ACKR= RST
The handshake completes in three packets; the fourth is data flowing.
Latency Implications
Each step of the handshake adds one RTT to the connection setup time:
- Step 1 → 2 = 1 RTT (client to server, then server to client).
- Step 2 → 3 = 0 RTT additional (client can include data with the ACK).
Total: 1 RTT before data flows.
Add TLS on top: another 1 RTT for TLS 1.3. Add HTTP: another 1 RTT for request/response. Total ~3 RTTs from “connect” to “data received.”
This is why HTTP/3 with QUIC is faster — it combines transport and TLS into one handshake, saving an RTT.
TFO: TCP Fast Open
An optimization that sends data in the initial SYN. The client and server cache a cookie from a previous connection; subsequent connections can send data in the SYN itself.
Result: 0-RTT data exchange for repeat connections.
Adoption: real but limited. Some middleboxes drop unexpected SYN payloads. Mostly used in specific environments.
Common Connection Issues
”Connection refused”
Server isn’t listening on the port. RST in response to SYN.
”Connection timed out”
SYN sent; no response. Server unreachable, firewall blocking, or down.
”Connection reset”
Established connection abruptly aborted (RST from peer or middlebox).
Very slow handshake
Network path is high-latency, or PMTUD issues forcing fragmentation. See MTU and MSS.
Many connections in CLOSE-WAIT
Application not closing connections after peer’s FIN. Resource leak.
Port exhaustion
Many connections in TIME-WAIT; system out of ephemeral ports. Tune kernel settings or use connection pooling.
TL;DR
- TCP handshake = SYN, SYN-ACK, ACK. Three packets, 1 RTT.
- Negotiates initial sequence numbers, window size, MSS, options.
- TIME-WAIT keeps sockets around for 2× MSL after close.
- Slow start ramps up bandwidth; small requests pay this cost.
- SYN flood attacks exploit half-open connections; SYN cookies mitigate.
- TLS adds an additional RTT before data flows; QUIC combines them.
- TCP Fast Open is a 0-RTT optimization for repeat connections.
The TCP handshake is the foundation of every reliable connection on the internet. Understanding it makes a lot of “why is my app slow on first request” or “why does my server have N thousand TIME-WAIT sockets” questions answerable. For the broader transport comparison, see TCP vs UDP; for the encryption that usually rides on top, TLS handshake.