Loading learning content...
Sequence numbers tell the sender which bytes it's transmitting. But how does the sender know if those bytes arrived? The answer lies in acknowledgment numbers—the receiver's way of saying "I've received everything up to this point."
Acknowledgment numbers complete TCP's reliability feedback loop. The sender transmits data, the receiver acknowledges it, and based on those acknowledgments, the sender knows whether to continue transmitting or retransmit lost data. Without acknowledgments, the sender would be transmitting blindly into the void.
This page explores acknowledgment numbers in depth: their semantics, their role in reliable delivery, the feedback loop they create, and the nuances that affect TCP performance.
By the end of this page, you will understand: the precise meaning of the TCP acknowledgment number, how receivers generate ACKs, how senders interpret ACKs to track progress, the relationship between sequence and acknowledgment numbers, piggybacking of ACKs on data segments, and the role of the ACK flag in TCP communication.
The TCP acknowledgment number has a precise, critical meaning:
Definition: The acknowledgment number specifies the next sequence number the receiver expects.
This means:
Important: The ACK number does NOT mean "I received byte N"—it means "I'm waiting for byte N because I've received everything before it."
Understanding ACK Semantics:═══════════════════════════════════════════════════════════════════ Received segment: SEQ=1000, LEN=500 (bytes 1000-1499) Receiver's response: ACK=1500 ✓ Means: "I received bytes 1000, 1001, ..., 1499" ✓ Means: "Send me byte 1500 next" ✗ Does NOT mean: "I received byte 1500" ───────────────────────────────────────────────────────────────── Common Interpretation Errors: Wrong: "ACK=1500 confirms receipt of byte 1500" Right: "ACK=1500 confirms receipt of bytes BEFORE 1500" Wrong: "ACK=1500 means the last received byte was 1500" Right: "ACK=1500 means the last received byte was 1499" ───────────────────────────────────────────────────────────────── Mathematical Relationship: If receiver got segment with SEQ=S and LEN=L (all in order): New ACK = S + L (Next expected = Start + Length of what was received)Always interpret ACK numbers as 'next expected byte.' This mental model prevents off-by-one errors when analyzing TCP traces. ACK=1000 means bytes 0-999 are received (assuming ISN=0), and byte 1000 is expected next.
The TCP header contains an ACK flag bit that indicates whether the acknowledgment number field is valid.
ACK Flag Rules:
In practice, the ACK flag is set on almost every segment after the initial SYN:
| Segment Type | ACK Flag | SEQ Field | ACK Field | Purpose |
|---|---|---|---|---|
| Initial SYN | 0 | Client ISN | Ignored | Request connection, send client's ISN |
| SYN-ACK | 1 | Server ISN | Client ISN + 1 | Accept connection, send server's ISN, ACK client's SYN |
| Connection ACK | 1 | Client ISN + 1 | Server ISN + 1 | Complete handshake, ACK server's SYN |
| Data segment | 1 | Current SEQ | Current ACK | Carry data and acknowledge received data |
| Pure ACK | 1 | Current SEQ | Current ACK | Acknowledge data without sending new data |
| FIN | 1 | Current SEQ | Current ACK | Request close, acknowledge any pending data |
Pure ACKs vs. Piggybacked ACKs:
Piggybacking is efficient—why send a separate ACK when you can include it in a data segment you're sending anyway? TCP prefers piggybacking but will send pure ACKs when:
A pure ACK segment (no data, no SYN, no FIN) doesn't advance the sender's sequence number. The ACK's SEQ field shows the sender's current position but no sequence byte is consumed. This is why pure ACKs don't need to be acknowledged themselves—they carry no data that could be lost.
How does a receiver decide what acknowledgment number to send? The process depends on what data has been received:
In-Order Segment Arrival:
When a segment arrives with SEQ = RCV.NXT (exactly what was expected):
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
class TCPReceiver: """ Demonstrates TCP receiver ACK generation logic. """ def __init__(self, initial_irs: int): self.irs = initial_irs # Initial Receive Sequence self.rcv_nxt = initial_irs + 1 # Next expected (after SYN) self.out_of_order_buf = {} # SEQ -> data for OOO segments def receive_segment(self, seq: int, data_len: int) -> int: """ Process a received segment and return the ACK number to send. Args: seq: Sequence number of received segment data_len: Length of data in segment (bytes) Returns: ACK number to send in response """ # Case 1: In-order segment if seq == self.rcv_nxt: # Accept and advance RCV.NXT self.rcv_nxt = seq + data_len # Check if buffered out-of-order data is now contiguous while self.rcv_nxt in self.out_of_order_buf: buffered_len = self.out_of_order_buf.pop(self.rcv_nxt) self.rcv_nxt += buffered_len return self.rcv_nxt # ACK the new expected byte # Case 2: Out-of-order segment (gap exists) elif seq > self.rcv_nxt: # Buffer for later, ACK still requests missing data self.out_of_order_buf[seq] = data_len return self.rcv_nxt # ACK unchanged - still waiting for gap # Case 3: Duplicate or old segment else: # seq < self.rcv_nxt # Already received, send current ACK return self.rcv_nxt # ACK unchanged # Example: In-order arrivalreceiver = TCPReceiver(initial_irs=1000)print(f"Initial RCV.NXT: {receiver.rcv_nxt}") ack1 = receiver.receive_segment(seq=1001, data_len=500)print(f"After SEQ=1001, LEN=500: ACK={ack1}") # ACK=1501 ack2 = receiver.receive_segment(seq=1501, data_len=1000)print(f"After SEQ=1501, LEN=1000: ACK={ack2}") # ACK=2501 # Example: Out-of-order arrivalreceiver2 = TCPReceiver(initial_irs=1000)ack_ooo1 = receiver2.receive_segment(seq=2001, data_len=500)print(f"\nOut-of-order SEQ=2001: ACK={ack_ooo1}") # ACK=1001 (gap!) ack_ooo2 = receiver2.receive_segment(seq=1001, data_len=1000)print(f"Fill gap SEQ=1001: ACK={ack_ooo2}") # ACK=2501 (gap filled!)Out-of-Order Segment Arrival:
When a segment arrives with SEQ > RCV.NXT (a gap exists):
This is critical: even when receiving valid data, the ACK "sticks" at the gap point until the gap is filled. This informs the sender that something is missing.
When out-of-order segments arrive, the receiver keeps sending the same ACK (requesting the missing byte). These repeated ACKs are called 'duplicate ACKs.' Three duplicate ACKs trigger fast retransmit—the sender retransmits the missing segment without waiting for a timeout. This is one of TCP's most important performance optimizations.
When the sender receives an ACK, it extracts valuable information about transmission progress:
Basic Processing:
Sender ACK Processing States: Initial State: SND.UNA = 1000 (oldest unacked byte) SND.NXT = 3000 (next byte to send) Send buffer: bytes 1000-2999 retained ═══════════════════════════════════════════════════════════════ Scenario A: Progressive ACKs (normal operation)───────────────────────────────────────────────────────────────Receive ACK=1500: ACK > SND.UNA? 1500 > 1000 ✓ Action: Advance SND.UNA to 1500 Free bytes 1000-1499 from buffer Reset retransmission timer New state: SND.UNA = 1500 Receive ACK=2000: ACK > SND.UNA? 2000 > 1500 ✓ Action: Advance SND.UNA to 2000 Free bytes 1500-1999 from buffer Reset retransmission timer New state: SND.UNA = 2000 ═══════════════════════════════════════════════════════════════ Scenario B: Duplicate ACKs (possible loss)───────────────────────────────────────────────────────────────State: SND.UNA = 1000 Receive ACK=1000 (1st): ACK == SND.UNA, no progressReceive ACK=1000 (2nd): Duplicate ACK count = 1Receive ACK=1000 (3rd): Duplicate ACK count = 2Receive ACK=1000 (4th): Duplicate ACK count = 3 → FAST RETRANSMIT! Action: Retransmit segment starting at byte 1000 immediately ═══════════════════════════════════════════════════════════════ Scenario C: Old ACK (already processed)───────────────────────────────────────────────────────────────State: SND.UNA = 2000 (we've already gotten ACK=2000) Receive ACK=1500: ACK < SND.UNA Action: Ignore (this is old, delayed ACK)ACKs and Buffer Management:
The sender must retain transmitted data until it's acknowledged—in case retransmission is needed. The ACK number directly controls buffer freeing:
This is why slow or missing ACKs can cause the send buffer to fill up, eventually blocking the application from sending more data.
ACK arrival is the heartbeat of TCP. It triggers buffer management, timer resets, congestion window adjustments, and retransmission decisions. When troubleshooting TCP performance, always examine ACK patterns—they reveal the health of the connection.
Sending an ACK for every segment is simple but wasteful. Delayed ACKs (RFC 1122) optimize efficiency by batching acknowledgments.
How Delayed ACKs Work:
RFC 1122 Requirements:
Delayed ACK Scenarios:═══════════════════════════════════════════════════════════════ Scenario 1: Two segments, one ACK─────────────────────────────────Time 0ms: Segment 1 (SEQ=1000, LEN=1000) arrives → Start delay timerTime 50ms: Segment 2 (SEQ=2000, LEN=1000) arrives → Cancel timer Second segment triggers immediate ACK Send: ACK=3000 (acknowledges both segments) Result: 2 segments, 1 ACK (50% reduction) ═══════════════════════════════════════════════════════════════ Scenario 2: Piggyback on response data─────────────────────────────────────Time 0ms: Request segment arrives → Start delay timerTime 100ms: Application generates response data Send: Response data with piggybacked ACK Result: ACK delivered with no additional segment ═══════════════════════════════════════════════════════════════ Scenario 3: Timer expiration (no batching opportunity)─────────────────────────────────────────────────────Time 0ms: Segment 1 arrives → Start 200ms delay timerTime 200ms: Timer expires, no second segment, no outgoing data Send: Pure ACK=2000 Result: 200ms delay added to ACK delivery ═══════════════════════════════════════════════════════════════ Scenario 4: Out-of-order (immediate ACK required)─────────────────────────────────────────────────────Expected: SEQ=1000Received: SEQ=2000 (gap at 1000!) Immediate action: Send ACK=1000 (do NOT delay!) Signals loss, may trigger fast retransmitNagle's algorithm delays sending small segments until prior data is acknowledged. Delayed ACKs delay acknowledgments until more data arrives or a timer fires. Together, they can cause 200ms delays on every small request-response exchange. Solutions include TCP_NODELAY (disables Nagle) or TCP_QUICKACK (disables delayed ACKs temporarily).
TCP supports full-duplex communication—both endpoints can send data simultaneously. This means every segment can carry both outgoing data (using SEQ) and acknowledgments for received data (using ACK).
The Dual Purpose of Every Segment:
Most TCP segments after connection establishment serve dual purposes:
Asymmetric Traffic Patterns:
Not all connections have symmetric data flow. Consider:
In asymmetric patterns, the light-sender still transmits segments—primarily to carry acknowledgments for the heavy-sender's data. These become mostly-ACK segments with minimal or no data payload.
Asymmetric Traffic: File Download═══════════════════════════════════════════════════════════════ Client sends: Server sends: Request (small), File data (large), ACKs for file data ACKs for request Segment flow: Client → Server: SEQ=1000, LEN=50, ACK=5000 [HTTP GET request]Server → Client: SEQ=5000, LEN=1460, ACK=1050 [File chunk 1]Server → Client: SEQ=6460, LEN=1460, ACK=1050 [File chunk 2]Client → Server: SEQ=1050, LEN=0, ACK=7920 [Pure ACK]Server → Client: SEQ=7920, LEN=1460, ACK=1050 [File chunk 3]Server → Client: SEQ=9380, LEN=1460, ACK=1050 [File chunk 4]Client → Server: SEQ=1050, LEN=0, ACK=10840 [Pure ACK]... Observations:─────────────────────────────────────────────────────────────────1. Server ACKs stay constant (1050) - client sent no new data2. Client has no data to send, uses pure ACKs3. Server piggybacks ACK on every data segment (free!)4. Client sends ACK every ~2 segments (delayed ACK)During bulk download, the sender generates data segments at high rate, each piggybacking an ACK (essentially free). The receiver generates pure ACKs at lower rate (delayed ACK batching). This asymmetry is efficient: more segments flow toward the data sink, fewer toward the data source.
The ACK number and Window field work together to define what the sender is allowed to transmit:
Send Window Calculation:
The sender's usable window is:
Usable Window = ACK + Window - SND.NXT
Where:
The sender can transmit bytes from SND.NXT up to ACK + Window - 1.
ACK + Window = Right Edge of Send Window═══════════════════════════════════════════════════════════════ Example State: Received: ACK=10000, Window=50000 Current: SND.NXT=25000 Allowed sending range: [25000, 59999] (35000 bytes available) Left edge: SND.NXT = 25000 (next to send) Right edge: ACK + Window - 1 = 10000 + 50000 - 1 = 59999 ┌── SND.UNA (10000, acknowledged) │ ┌── SND.NXT (25000, next to send) │ │ ┌── Right edge (59999) ▼ ▼ ▼═══════════════════════════════════════════════════════════════│ Acknowledged │ In Flight │ Can Send │ Beyond Window ││ (freed) │ (awaiting ACK) │ (allowed) │ (blocked) │═══════════════════════════════════════════════════════════════ < 10000 10000 - 24999 25000 - 59999 ≥ 60000 ───────────────────────────────────────────────────────────────── Window Update Example: Current: ACK=10000, Window=50000 → Right edge = 60000 Later: ACK=20000, Window=40000 → Right edge = 60000 (unchanged!) The receiver acknowledged 10000 more bytes BUT reduced window by 10000 Net effect: Right edge stays the same This prevents "shrinking window" problemsWindow Shrinking Prohibition:
RFC 7323 strongly discourages (but doesn't absolutely prohibit) "shrinking" the right window edge. If ACK=10000, Window=50000 (right edge=60000), the next update should not make the right edge smaller than 60000. Shrinking could invalidate data the sender was allowed to send moments ago.
Zero Window:
When the receiver's buffer is full, it advertises Window=0. This tells the sender to stop completely. The sender then "probes" periodically (window probe) with minimal data to check if the window has opened.
A pure ACK that advances ACK or increases Window can open transmission opportunities. The sender should check after every ACK if it can now send data that was previously blocked. This 'window opening' is how flow control releases held traffic.
Acknowledgment numbers complete the reliability loop. The sender transmits with sequence numbers; the receiver responds with acknowledgments. This bidirectional dance ensures every byte is accounted for. Let's consolidate the key insights:
What's Next:
We've seen that ACKs acknowledge contiguous receipt. But that's a simplification—real networks lose packets in the middle of streams. The next page explores cumulative ACKs in depth: how TCP's acknowledgment scheme handles gaps, why cumulative ACKs are both robust and limiting, and how Selective Acknowledgment (SACK) extends the model.
You now understand TCP acknowledgment numbers: their semantic meaning, how receivers generate them, how senders process them, delayed ACK optimizations, and their interaction with flow control. Next, we examine cumulative acknowledgments and their implications for loss recovery.