TCP Sequence Numbers & Acknowledgements How TCP Guarantees Reliable Delivery

 

TCP Sequence Numbers & Acknowledgements
How TCP Guarantees Reliable Delivery — Chapter 61, Part 3

← Chapter 61 Index  |  Part 3 of 3

The Core Problem TCP Solves

IP is unreliable by design — it does its best to deliver packets but makes no guarantee. Packets can be lost, duplicated, reordered, or corrupted in transit. TCP is built on top of IP and adds reliability: every byte you send is guaranteed to arrive, in order, exactly once.

TCP achieves this using three mechanisms working together:

  • Sequence Numbers — each byte is numbered so the receiver can detect missing or out-of-order data.
  • Positive Acknowledgements — the receiver explicitly confirms what it has received.
  • Retransmission Timers — if no ACK arrives in time, the sender retransmits.

How Sequence Numbers Work

When a TCP connection is established, each side picks a random starting number called the Initial Sequence Number (ISN). From that point, every byte of data sent is assigned the next sequential number. This numbering is per-direction — each side maintains its own independent sequence counter.

Byte number in stream Sequence number (ISN = 1000) Data byte
Byte 1 Seq = 1001 ‘H’
Byte 2 Seq = 1002 ‘e’
Byte 3 Seq = 1003 ‘l’
Byte 4 Seq = 1004 ‘l’
Byte 5 Seq = 1005 ‘o’

When TCP sends a segment carrying multiple bytes, the segment’s Sequence Number field is set to the sequence number of the first byte in that segment. The receiver uses this to place received bytes in the correct position.

/* Conceptual illustration of segment sequence numbers */

Segment 1: Seq = 1001, length = 500 bytes  → carries bytes 1001 to 1500
Segment 2: Seq = 1501, length = 500 bytes  → carries bytes 1501 to 2000
Segment 3: Seq = 2001, length = 300 bytes  → carries bytes 2001 to 2300

Positive Acknowledgements — How the Receiver Responds

When the receiver successfully accepts a segment, it sends back an ACK segment (a segment with the ACK flag set). The key value in this ACK is the Acknowledgement Number field.

The ACK Number Rule
ACK number = (sequence number of last byte received) + 1
This is the sequence number of the next byte the receiver is expecting. It is a compact way to say “I have everything up to N−1, please send me N next.”

Step-by-Step ACK Flow

Sender (Host A) Action Receiver (Host B)
Sends segment
Seq=1001, Len=500
──────────────────→
Receives bytes 1001–1500
Receives ACK 1501.
Moves window forward.
←─────────────────
ACK=1501 (Ack #)
Sends ACK: Ack#=1501
(“send me byte 1501 next”)
Sends segment
Seq=1501, Len=500
──────────────────→
Receives bytes 1501–2000
Receives ACK 2001.
All data acknowledged.
←─────────────────
ACK=2001
Sends ACK: Ack#=2001

Retransmission: What Happens When a Segment Is Lost

Every time the sender transmits a segment, it starts a retransmission timer. If an acknowledgement does not arrive before the timer expires, TCP assumes the segment was lost and retransmits the same data.

Sender Network Receiver
Sends Seq=1001, starts timer → LOST ✗ Never receives this segment
Timer expires — no ACK received …time passes… — (nothing)
Retransmits Seq=1001 ──────────→ Receives it, sends ACK=1501

The retransmission timeout (RTO) is not fixed. TCP calculates it dynamically based on the measured round-trip time (RTT). If retransmission is needed multiple times, TCP applies exponential backoff — doubling the timeout each time — to avoid flooding an already congested network.

Cumulative vs Selective ACK

Cumulative ACK (default)

The ACK number acknowledges all bytes up to and including that point. If segments arrive out of order (e.g., 1, 3, 4 but not 2), the receiver keeps sending ACK=2 (“still waiting for byte 2”). When byte 2 finally arrives, a single ACK=5 acknowledges segments 2, 3, and 4 all at once.

Selective ACK / SACK (optional)

With SACK (negotiated during SYN via TCP options), the receiver can say “I have bytes 1–500 and 1001–1500 but missing 501–1000”. The sender can then retransmit only the missing range instead of retransmitting everything from 501 onward. SACK greatly improves performance when multiple segments are lost.

Demonstrating Sequence Numbers: A Practical TCP Sender Simulation

While you cannot manually set TCP sequence numbers in normal socket programming (the kernel handles them), understanding the logic helps debug and reason about your applications. Below is a C program that simulates the sender-side logic for educational purposes.

#include <stdio.h>
#include <stdint.h>
#include <string.h>

/*
 * Simulate how TCP tracks sequence numbers.
 * In real TCP, the kernel does this — but understanding it
 * helps you debug Wireshark captures and race conditions.
 */

typedef struct {
    uint32_t isn;        /* initial sequence number */
    uint32_t send_next;  /* next seq# to send */
    uint32_t send_una;   /* oldest unacknowledged seq# */
    uint32_t window;     /* peer's receive window */
} tcp_send_state_t;

typedef struct {
    uint32_t rcv_next;   /* next expected seq# */
    uint32_t rcv_window; /* our receive window */
} tcp_recv_state_t;

/* Sender: prepare a segment */
void sender_send(tcp_send_state_t *s, uint32_t data_len)
{
    printf("[SENDER] Sending segment: Seq=%u  Len=%u  (Seq range: %u to %u)\n",
           s->send_next,
           data_len,
           s->send_next,
           s->send_next + data_len - 1);

    s->send_next += data_len;
}

/* Receiver: accept a segment and generate ACK */
uint32_t receiver_accept(tcp_recv_state_t *r, uint32_t seg_seq, uint32_t seg_len)
{
    if (seg_seq != r->rcv_next) {
        printf("[RECEIVER] OUT OF ORDER: expected Seq=%u got Seq=%u — holding\n",
               r->rcv_next, seg_seq);
        /* In real TCP, this would trigger duplicate ACK */
        return r->rcv_next;
    }

    r->rcv_next += seg_len;

    uint32_t ack_num = r->rcv_next;
    printf("[RECEIVER] Accepted bytes %u–%u. Sending ACK=%u\n",
           seg_seq, seg_seq + seg_len - 1, ack_num);
    return ack_num;
}

/* Sender: process an incoming ACK */
void sender_process_ack(tcp_send_state_t *s, uint32_t ack_num)
{
    if (ack_num > s->send_una) {
        printf("[SENDER]   ACK=%u received. Data bytes %u–%u are now acknowledged.\n\n",
               ack_num, s->send_una, ack_num - 1);
        s->send_una = ack_num;
    }
}

int main(void)
{
    tcp_send_state_t sender = {
        .isn       = 1000,
        .send_next = 1001,   /* first data byte after SYN consumed ISN+1 */
        .send_una  = 1001,
        .window    = 65535
    };

    tcp_recv_state_t receiver = {
        .rcv_next   = 1001,
        .rcv_window = 65535
    };

    printf("=== TCP Sequence / ACK Simulation ===\n\n");
    printf("ISN = %u, first data byte seq = %u\n\n", sender.isn, sender.send_next);

    /* Send segment 1: 500 bytes */
    sender_send(&sender, 500);
    uint32_t ack = receiver_accept(&receiver, 1001, 500);
    sender_process_ack(&sender, ack);

    /* Send segment 2: 500 bytes */
    sender_send(&sender, 500);
    ack = receiver_accept(&receiver, 1501, 500);
    sender_process_ack(&sender, ack);

    /* Simulate out-of-order delivery */
    printf("--- Simulating out-of-order segment ---\n");
    sender_send(&sender, 300);
    ack = receiver_accept(&receiver, 2500, 300);  /* wrong seq, should be 2001 */
    sender_process_ack(&sender, ack);

    /* Correct segment arrives */
    printf("--- Correct segment arrives ---\n");
    ack = receiver_accept(&receiver, 2001, 300);
    sender_process_ack(&sender, ack);

    return 0;
}

Compile and run:

gcc -o tcp_sim tcp_sim.c
./tcp_sim

Expected output:

=== TCP Sequence / ACK Simulation ===

ISN = 1000, first data byte seq = 1001

[SENDER] Sending segment: Seq=1001  Len=500  (Seq range: 1001 to 1500)
[RECEIVER] Accepted bytes 1001–1500. Sending ACK=1501
[SENDER]   ACK=1501 received. Data bytes 1001–1500 are now acknowledged.

[SENDER] Sending segment: Seq=1501  Len=500  (Seq range: 1501 to 2000)
[RECEIVER] Accepted bytes 1501–2000. Sending ACK=2001
[SENDER]   ACK=2001 received. Data bytes 1501–2000 are now acknowledged.

--- Simulating out-of-order segment ---
[SENDER] Sending segment: Seq=2001  Len=300  (Seq range: 2001 to 2300)
[RECEIVER] OUT OF ORDER: expected Seq=2001 got Seq=2500 — holding
--- Correct segment arrives ---
[RECEIVER] Accepted bytes 2001–2300. Sending ACK=2301
[SENDER]   ACK=2301 received. Data bytes 2001–2300 are now acknowledged.

Why Is the ISN Random?

If TCP always started sequence numbers at 0 or 1, an attacker could inject fake segments into an existing connection by guessing the current sequence number. A random ISN makes this impractical — the attacker would need to guess a 32-bit number correctly. On Linux, the ISN is generated using a secure algorithm based on time and a per-connection hash. The random start also prevents confusion between new and old connections that use the same port pair.

Special Rule: SYN and FIN Each Consume One Sequence Number

Even though SYN and FIN segments carry no actual data, they each consume one sequence number. This means the receiver’s ACK for a SYN will be ISN+1, not ISN. This is why the first real data byte has sequence number ISN+1.

/* Connection establishment sequence numbers */

Client sends:    SYN,  Seq = 1000  (ISN)
Server replies:  SYN+ACK, Seq = 5000 (server ISN), Ack = 1001  (1000+1)
Client sends:    ACK, Seq = 1001, Ack = 5001  (5000+1)

/* Now the connection is ESTABLISHED */
/* First data from client will have Seq = 1001 */
/* First data from server will have Seq = 5001 */

Observing Sequence Numbers with tcpdump

You can watch real TCP sequence numbers on your own machine using tcpdump. The -S flag shows absolute sequence numbers (otherwise tcpdump shows relative numbers for readability).

# In terminal 1: start a web server (or any TCP server)
python3 -m http.server 8080

# In terminal 2: capture TCP on port 8080
sudo tcpdump -i lo -S -n 'port 8080'

# In terminal 3: make a request
curl http://localhost:8080/

# tcpdump output will show something like:
# 12:00:00 IP 127.0.0.1.54321 > 127.0.0.1.8080: Flags [S],  seq 3491827410
# 12:00:00 IP 127.0.0.1.8080  > 127.0.0.1.54321: Flags [S.], seq 2183940271, ack 3491827411
# 12:00:00 IP 127.0.0.1.54321 > 127.0.0.1.8080: Flags [.],  ack 2183940272
# (data segments follow...)

Notice that Flags [S.] means SYN + ACK, and the ack is always the peer’s seq + 1.

Interview Questions and Answers

Q1. What is the purpose of sequence numbers in TCP?
Sequence numbers let the receiver detect and handle three conditions: (1) missing segments — a gap in sequence numbers means a loss occurred; (2) duplicate segments — if the same sequence number arrives twice, discard the second copy; (3) reordered segments — use sequence numbers to reassemble the stream in the correct order before delivering to the application.
Q2. How is the ACK number calculated?
ACK number = sequence number of the last byte successfully received + 1. It is the sequence number of the next byte the receiver is expecting. If segment carried bytes 1001 to 1500, the ACK number sent back is 1501.
Q3. What is a positive acknowledgement and how does it differ from a negative ACK?
TCP uses positive acknowledgements — the receiver only sends an ACK when data is successfully received. There is no “NACK” (negative acknowledgement) in TCP. The absence of an ACK (detected by timeout) is how the sender infers loss. Some other protocols like some ARQ schemes do use explicit NACKs, but standard TCP does not.
Q4. What happens when a retransmission timer expires?
The sender retransmits the unacknowledged segment and doubles the timeout (exponential backoff). This prevents a congested network from being flooded with retransmissions. After several retransmissions without an ACK, TCP gives up and reports a connection timeout error to the application. On Linux, this appears as ETIMEDOUT.
Q5. Why is the Initial Sequence Number (ISN) random?
Two reasons. First, security: a predictable ISN allows an attacker to inject forged segments into an established connection by guessing the current sequence number. Second, correctness: if a connection closes and a new one is opened immediately with the same port pair, old duplicate packets from the previous connection would be confused with data from the new one. A random ISN makes old packets invalid in the new connection.
Q6. Does a SYN segment consume a sequence number even though it carries no data?
Yes. Both SYN and FIN each consume one sequence number, even though they carry no data payload. This is why the ACK for a SYN is ISN+1, and the ACK for a FIN is the FIN’s sequence number +1. It allows the receiver to acknowledge the SYN/FIN themselves, not just data bytes.
Q7. What is SACK and when is it useful?
SACK (Selective Acknowledgement) is a TCP option negotiated during the SYN exchange. With plain cumulative ACKs, if segment 2 is lost but segments 3, 4, 5 arrive, the receiver keeps ACKing “send me 2” — the sender may retransmit all of 2, 3, 4, 5. With SACK, the receiver can say “I have 3, 4, 5 but missing 2” — the sender retransmits only 2. SACK is especially valuable on high-bandwidth, high-latency links where multiple segments can be lost in one window.
Q8. How does TCP detect and discard duplicate segments?
The receiver checks the sequence number of every arriving segment against its expected next sequence number (rcv_next). If a segment’s sequence range overlaps with data already received, the receiver trims or discards the duplicate bytes. The random ISN also protects against very old duplicate packets from a previous connection appearing in a new one (in conjunction with the TIME_WAIT state).

Chapter 61 Part 3 Summary

  • Every byte in a TCP stream has a sequence number starting from a random ISN.
  • The receiver sends a positive ACK = (last received byte seq) + 1.
  • The sender starts a timer; if no ACK arrives, it retransmits with exponential backoff.
  • SYN and FIN each consume one sequence number even with no data payload.
  • Cumulative ACKs are standard; SACK is an optional extension for better loss recovery.
  • Use tcpdump -S to see real sequence and ACK numbers on your own system.

Leave a Reply

Your email address will not be published. Required fields are marked *