TCP Three-Way Handshake
ACK: Completing the Handshake
Read aloud is unavailable in this browser.
The Final Step: From Half-Open to ESTABLISHED
The three-way handshake culminates in the client's final ACK segment—a deceptively simple packet that carries enormous significance. This third message transforms a tentative, half-open connection into a fully ESTABLISHED bidirectional communication channel.
Until this ACK arrives, the server sits in a vulnerable SYN_RECEIVED state: resources allocated, sequence number proposed, but no confirmation that the client is still alive and ready to communicate. The final ACK delivers that confirmation, triggering the last state transition and enabling the flow of application data.
By the end of this page, you will understand the precise role of the final ACK in the handshake, the ESTABLISHED state transition on both client and server, how data can be piggybacked on the ACK, what happens when the ACK is lost, and the critical timing considerations for connection establishment.
The Purpose of the Final ACK
The final ACK of the three-way handshake serves a single but critical purpose: confirming to the server that the client received the SYN-ACK. This confirmation is essential for several reasons:
1. Validating Bidirectional Communication:
After the SYN-ACK, we know:
- ✓ Client → Server works (the SYN arrived)
- ✓ Server → Client works (the SYN-ACK arrived)
- ✗ Server doesn't know its SYN-ACK arrived until ACK confirms it
The final ACK completes the validation loop, proving both directions are functional.
2. Acknowledging the Server's ISN:
The SYN-ACK contained the server's Initial Sequence Number. Without an acknowledgment, the server can't be sure the client knows where to expect the server's first data byte. The ACK's acknowledgment number (server's ISN + 1) confirms this.
3. Releasing the Server from SYN_RECEIVED:
While in SYN_RECEIVED, the server has committed resources but can't transfer data. The ACK transitions the server to ESTABLISHED, enabling data transfer and freeing it from the handshake's vulnerability window.
When the client receives the SYN-ACK, it knows the server is ready. Why must it send an explicit ACK before data? Because the server is stuck in SYN_RECEIVED until it receives something that acknowledges its ISN. The client could send data (with the ACK flag set), but it must include the acknowledgment. The 'pure ACK' is the minimal packet that completes the handshake; data can optionally accompany it.
The Acknowledgment Semantics:
In the final ACK:
Acknowledgment Number = Server's ISN + 1
Just as the server's SYN-ACK acknowledged the client's SYN (client_ISN + 1), the client's ACK acknowledges the server's SYN-ACK (server_ISN + 1). This symmetry is elegant and intentional:
| Segment | Acknowledges |
|---|---|
| SYN | Nothing (first message) |
| SYN-ACK | Client's SYN |
| ACK | Server's SYN-ACK |
Structure of the Final ACK Segment
The final ACK is typically the simplest packet in the handshake—often just a TCP header with no data and no options. Let's examine its structure:
| Field | Size | Value | Explanation |
|---|---|---|---|
| Source Port | 16 bits | Ephemeral port (e.g., 54321) | Client's port (same as in SYN) |
| Destination Port | 16 bits | Service port (e.g., 443) | Server's port (same as in SYN) |
| Sequence Number | 32 bits | Client's ISN + 1 | Next byte client would send |
| Acknowledgment Number | 32 bits | Server's ISN + 1 | Acknowledges server's SYN-ACK |
| Data Offset | 4 bits | 5 (20 bytes, no options) | Minimal header with no options |
| Reserved | 4 bits | 0 | Reserved for future use |
| Flags | 8 bits | 0x10 (ACK only) | Only ACK flag set |
| Window Size | 16 bits | Receive window | Current receive buffer availability |
| Checksum | 16 bits | Computed value | Integrity check |
| Urgent Pointer | 16 bits | 0 | Not valid when URG not set |
| Options | None | N/A | Options already negotiated |
| Data | 0 bytes (usually) | N/A | Can optionally carry first data |
Key Observations:
-
Flags = 0x10 (ACK only) — Unlike the SYN-ACK which had both flags set (0x12), the final ACK has only the ACK flag. The binary representation: 0001 0000 = 0x10.
-
Sequence Number = Client's ISN + 1 — The SYN consumed sequence number ISN. The next sequence number (for data or control) is ISN + 1. Even though this pure ACK carries no data, the sequence field indicates where data would start.
-
No Options (Typically) — Option negotiation is complete. The final ACK usually carries no options, resulting in a minimal 20-byte header. Some implementations may include Timestamps if negotiated.
-
Window Update — The window size reflects the client's current receive buffer availability. This is the first 'real' window advertisement since the handshake is completing.
A pure handshake ACK has zero data bytes. However, nothing prevents piggybacking application data on this segment. If the client has data ready to send (e.g., an HTTP GET request), it can include that data with the ACK, saving one round-trip. This is common and correct behavior.
State Transitions to ESTABLISHED
The final ACK triggers the last state transition in connection establishment. However, the timing is subtly different for client and server:
Client's Perspective:
The client transitions to ESTABLISHED when it sends the final ACK, not when it's confirmed received. From the TCP state machine:
SYN_SENT → (receive SYN-ACK, send ACK) → ESTABLISHED
This means the client enters ESTABLISHED before the server does. The client can immediately start sending data (though it may be buffered until the three-way handshake completes).
Server's Perspective:
The server transitions to ESTABLISHED when it receives the final ACK:
SYN_RECEIVED → (receive ACK) → ESTABLISHED
Until this ACK arrives, the server remains in the vulnerable half-open state.
- •T=0: CLOSED → SYN_SENT (send SYN)
- •T=RTT/2: Receive SYN-ACK
- •T=RTT/2: Send ACK
- •T=RTT/2: SYN_SENT → ESTABLISHED
- •T=RTT/2+: Can send/receive data
- •T=0: LISTEN (waiting for SYN)
- •T=RTT/2: Receive SYN
- •T=RTT/2: LISTEN → SYN_RECEIVED (send SYN-ACK)
- •T=RTT: Receive ACK
- •T=RTT: SYN_RECEIVED → ESTABLISHED
The Asymmetry Matters:
The client enters ESTABLISHED half an RTT before the server. This has practical implications:
-
Client can send data immediately — Data sent after the ACK may arrive at the server before the ACK does. The server will buffer this data until it enters ESTABLISHED.
-
Connection accept timing — The application's
accept()call returns only when the connection reaches ESTABLISHED. Server-side timers measuring 'time to first byte' should account for this. -
Error handling — If the ACK is lost and the server eventually times out, the client thinks the connection is open while the server has abandoned it. Keepalives or data transmission will eventually detect this.
The ESTABLISHED State:
Once ESTABLISHED, both sides can:
- Send and receive application data
- Acknowledge received data
- Perform flow and congestion control
- Eventually close the connection gracefully
The ESTABLISHED state persists until one side initiates connection termination (FIN) or an error occurs (RST). A connection can remain ESTABLISHED for seconds, minutes, or months—TCP has no inherent connection timeout. This is why server-side idle timeouts and keepalives are important for resource management.
Data Piggybacking on the Final ACK
One of TCP's elegant optimizations is the ability to piggyback data on acknowledgment segments. The final handshake ACK is no exception—application data can (and often should) be included.
Why Piggyback Data?
Without piggybacking:
Client: SYN
Server: SYN-ACK
Client: ACK (empty) ← Packet #3
Client: DATA (GET /index.html) ← Packet #4
With piggybacking:
Client: SYN
Server: SYN-ACK
Client: ACK + DATA (GET /index.html) ← Packet #3 with data
Piggybacking saves one packet, reducing latency and network overhead.
| Aspect | Without Piggybacking | With Piggybacking |
|---|---|---|
| Packets for handshake + first request | 4 | 3 |
| Time to first request | 1.5 RTT + processing | 1.5 RTT |
| Header overhead | 80 bytes (2 headers) | 40 bytes (1 header) |
| Network utilization | Lower | Higher |
| Implementation complexity | Simpler | Slightly complex |
The Piggybacked ACK Structure:
When data is included, the segment looks like:
Flags: 0x18 (PSH + ACK)
Sequence Number: Client_ISN + 1
Acknowledgment: Server_ISN + 1
Data: Application payload (e.g., HTTP request)
Notice the PSH (Push) flag is typically set along with ACK, telling the receiver to deliver data to the application promptly rather than buffering.
Example: HTTP Request on Final ACK:
123456789101112131415
Transmission Control Protocol, Src Port: 54321, Dst Port: 80 Flags: 0x018 (PSH, ACK) .... ...1 .... = Acknowledgment: Set .... .... 1... = Push: Set Sequence Number: 1001 (relative: 1) Acknowledgment Number: 2001 (relative: 1) [TCP Segment Len: 85] HTTP Request (85 bytes): GET /index.html HTTP/1.1\r Host: www.example.com\r \r Standard TCP requires waiting for the SYN-ACK before sending data. TCP Fast Open (RFC 7413) allows data in the initial SYN itself for repeat connections, saving another RTT. The client caches a 'Fast Open Cookie' from the first connection; subsequent connections include data in the SYN, and the server can respond with data in the SYN-ACK.
Handling Lost Final ACKs
The final ACK, like any packet, can be lost in transit. This creates an interesting asymmetry: the client believes the connection is ESTABLISHED while the server remains in SYN_RECEIVED.
The Lost ACK Scenario:
| Time | Client State | Server State | Event |
|---|---|---|---|
| T=0 | CLOSED | LISTEN | Client initiates connection |
| T=0 | SYN_SENT | LISTEN | Client sends SYN |
| T=50ms | SYN_SENT | SYN_RECEIVED | Server receives SYN, sends SYN-ACK |
| T=100ms | ESTABLISHED | SYN_RECEIVED | Client receives SYN-ACK, sends ACK (lost) |
| T=3s | ESTABLISHED | SYN_RECEIVED | Server's timer expires |
| T=3s | ESTABLISHED | SYN_RECEIVED | Server retransmits SYN-ACK |
| T=3.05s | ESTABLISHED | SYN_RECEIVED | Client receives duplicate SYN-ACK |
| T=3.05s | ESTABLISHED | SYN_RECEIVED | Client retransmits ACK |
| T=3.1s | ESTABLISHED | ESTABLISHED | Server receives ACK, connection complete |
Recovery Mechanism:
-
Server retransmits SYN-ACK — After a timeout (typically ~3 seconds initially), the server sends another SYN-ACK since it hasn't received the expected ACK.
-
Client responds to duplicate SYN-ACK — When the ESTABLISHED client receives a SYN-ACK, it knows the server hasn't received the ACK. It retransmits the ACK (this is idempotent and safe).
-
Recovery succeeds — The retransmitted ACK reaches the server, completing the handshake.
If Client Sends Data:
Interestingly, if the client sends data before the ACK recovery:
- Client sends ACK + data (or just data with ACK flag)
- ACK is lost, data reaches server
- Server receives data segment with ACK flag set
- Server sees ACK = Server_ISN + 1, transitioning to ESTABLISHED
- Connection recovered without explicit ACK retransmission
This is why the server's SYN-ACK retransmit timer is important—it provides a fallback when the client's first acknowledgment is lost but no data follows immediately.
If repeated SYN-ACK retransmissions fail (client is gone, network partitioned), the server eventually abandons the connection. Typical defaults: 5 retries with exponential backoff = ~30 seconds total. After the final retry, the server clears the TCB and returns to LISTEN for that port. The client, meanwhile, may still think it's connected until it tries to send and fails.
ACK Validation and Security Considerations
The server must carefully validate the incoming ACK to prevent attacks and ensure it corresponds to the correct half-open connection.
ACK Validation Checks:
- •Match the 4-tuple — Source IP, source port, destination IP, destination port must match a pending connection in SYN_RECEIVED state
- •Validate acknowledgment number — ACK must equal server's ISN + 1. Any other value indicates corruption, attack, or stale packet.
- •Check sequence number — Should be client's ISN + 1 (or higher if data included). Must fall within the valid receive window.
- •Verify checksum — TCP checksum must validate including the pseudo-header
- •Check for SYN/RST flags — A valid final ACK should not have SYN set (unless simultaneous open) and should not have RST set
Security Implications:
ACK Flooding Attack: An attacker sends many ACK packets with random acknowledgment numbers, hoping to match half-open connections and complete handshakes for spoofed IPs. Defenses:
- Cryptographic ISN generation makes guessing server's ISN infeasible
- Rate limiting on ACK acceptance
- SYN cookies (which don't have vulnerable SYN_RECEIVED state)
RST Attack (Connection Reset Attack): An off-path attacker who guesses the sequence and acknowledgment numbers can send a RST to tear down connections. This is why cryptographic ISN matters—it makes sequence space unpredictable.
SYN Cookie ACK Validation: When SYN cookies are active, the ACK validation is different:
- Server computes expected cookie value from ACK - 1
- Validates cookie against stored secret key and connection parameters
- If valid, creates TCB and enters ESTABLISHED
- If invalid, drops the ACK (no RST, to avoid being an amplifier)
With SYN cookies, the ACK validation effectively proves the client received the SYN-ACK—because the correct acknowledgment can only be computed by someone who received the cookie value. This elegantly prevents off-path attackers from completing handshakes to spoofed addresses.
Accept Queue and Application Notification
When the final ACK transitions a connection to ESTABLISHED, the operating system must notify the listening application. This involves the accept queue—a kernel data structure holding completed connections waiting for the application to accept them.
The Two Queues:
Modern TCP implementations maintain two queues for incoming connections:
- •Holds SYN_RECEIVED connections
- •Entries waiting for final ACK
- •Size controlled by kernel
- •Can be bypassed with SYN cookies
- •Vulnerable to SYN flood attacks
- •Holds ESTABLISHED connections
- •Waiting for application accept()
- •Size set by listen() backlog
- •If full, may drop new connections
- •Protected from SYN floods
Connection Lifecycle through Queues:
SYN arrives → SYN Queue entry created → SYN-ACK sent
↓
ACK arrives
↓
SYN Queue entry removed → Accept Queue entry created → ESTABLISHED
↓
Application calls accept()
↓
Connection socket returned
What Happens When Accept Queue Is Full:
If the accept queue is full when a connection completes:
| Kernel Behavior | Effect | Controlled By |
|---|---|---|
| Drop the connection | Client times out | Some systems |
| Send RST | Client sees 'Connection refused' | tcp_abort_on_overflow |
| Ignore the ACK | Server retransmits SYN-ACK | Default on Linux |
The default Linux behavior (ignoring the ACK) allows the connection to complete once the application catches up and frees accept queue space.
A slow application that doesn't call accept() fast enough can cause the accept queue to overflow. New connections are dropped even though the handshake completed. Monitor 'listen queue overflow' metrics (e.g., netstat -s | grep overflowed) and increase backlog size or improve application responsiveness if this occurs.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.