Loading learning content...
If port numbers answer "who is communicating," then sequence numbers answer "what position is this data in the stream?" Occupying bits 32–63 of the TCP header, the 32-bit sequence number field is the cornerstone of TCP's reliability guarantees.
The Internet Protocol makes no promises about packet delivery—packets can be lost, duplicated, reordered, or corrupted. TCP transforms this chaotic underlying network into a reliable, ordered byte stream. Sequence numbers are the mechanism that makes this transformation possible.
Every single byte of data sent over a TCP connection is assigned a unique sequence number. This enables the receiver to:
By the end of this page, you will understand TCP's byte-oriented sequence numbering system, how Initial Sequence Numbers (ISN) are selected and why randomization matters for security, the semantics of different segment types (SYN, FIN, data), sequence space and wraparound handling, and how sequence numbers integrate with acknowledgments to provide reliable delivery.
The sequence number field occupies the third 32-bit word of the TCP header, immediately following the port numbers. Its substantial size—32 bits—is deliberately chosen to support TCP's byte-oriented design across potentially massive data transfers.
| Bit Position | Field | Size | Description |
|---|---|---|---|
| 0–15 | Source Port | 16 bits | Port of sending process |
| 16–31 | Destination Port | 16 bits | Port of receiving process |
| 32–63 | Sequence Number | 32 bits | Position of first data byte in segment |
| 64–95 | Acknowledgment Number | 32 bits | Next expected byte from sender |
Why 32 Bits?
The 32-bit sequence number provides a range of 0 to 4,294,967,295 (2³² - 1), enabling:
| Consideration | Implication |
|---|---|
| Byte Addressing | Can uniquely identify ~4.3 billion bytes before wraparound |
| Large Transfers | Supports transfers of any size (sequence numbers wrap around) |
| Duplicate Detection | Windows and timestamps help disambiguate wrapped sequences |
| Historical Link Speeds | At 1970s speeds, wraparound took hours; at modern speeds, special handling needed |
The Challenge of High-Speed Networks:
At 10 Gbps, the sequence space wraps in approximately 3.4 seconds:
4,294,967,296 bytes ÷ 1,250,000,000 bytes/sec ≈ 3.44 seconds
This creates potential for old, delayed segments to arrive with sequence numbers that have "wrapped around" and now appear valid for the current window. TCP options like PAWS (Protection Against Wrapped Sequences) address this using timestamps.
TCP uses byte-oriented sequence numbers, not segment numbers. Each byte has a unique sequence number. The sequence number in a segment's header identifies the first byte of that segment's payload. This design allows segments of varying sizes and enables byte-granularity reliability.
TCP treats the data it transports as a continuous stream of bytes, not as discrete messages or records. Each byte in this stream is assigned a unique sequence number, starting from the Initial Sequence Number (ISN) negotiated during connection establishment.
The Sequence Number Formula:
Sequence Number of Byte N = ISN + N
Where:
The sequence number in a TCP segment's header indicates the sequence number of the first byte of payload in that segment.
Example: Tracking Bytes Across Segments
Consider a connection with ISN = 1000, sending "HELLO WORLD" (11 bytes) across three segments:
| Segment | Payload | Payload Size | Sequence Number | Bytes Covered |
|---|---|---|---|---|
| 1 | "HELLO" | 5 bytes | 1001 | 1001–1005 |
| 2 | " WOR" | 4 bytes | 1006 | 1006–1009 |
| 3 | "LD" | 2 bytes | 1010 | 1010–1011 |
Key Observations:
The next sequence number after a segment = current sequence number + payload length. For segment with Seq=1001 and length=5, the next expected is 1006. This is precisely what the receiver acknowledges.
The Initial Sequence Number (ISN) is the starting point for all sequence numbers in a TCP connection. Each direction of the full-duplex connection has its own independent ISN, selected during the three-way handshake.
Why Not Start at Zero?
It might seem logical to start sequence numbers at 0, but this would create serious problems:
The ISN Selection Problem:
RFC 793 (original TCP specification) recommended incrementing a system-wide counter every 4 microseconds to select ISNs. This made ISNs highly predictable, enabling the infamous 1985 "blind spoofing" attack where attackers could inject data into TCP connections without seeing responses.
Modern ISN Generation (RFC 6528):
The recommended approach uses a cryptographic construction:
ISN = M + F(local_ip, local_port, remote_ip, remote_port, secret_key)
Where:
This makes ISN prediction computationally infeasible without knowing the secret key, while still ensuring ISNs are sufficiently spread across the sequence space.
123456789101112131415161718192021222324252627282930313233343536373839404142
import hashlibimport timeimport os # Simplified RFC 6528-style ISN generationclass ISNGenerator: def __init__(self): # Per-boot secret (would be generated at system startup) self._secret = os.urandom(32) # Base time for M component self._boot_time = time.time() def generate_isn(self, local_ip: str, local_port: int, remote_ip: str, remote_port: int) -> int: """Generate cryptographically secure ISN for a connection.""" # M: Time component (4μs granularity simulation) elapsed_us = int((time.time() - self._boot_time) * 1_000_000) M = (elapsed_us // 4) % (2**32) # F: Cryptographic hash of connection tuple + secret connection_tuple = f"{local_ip}:{local_port}:{remote_ip}:{remote_port}" hash_input = connection_tuple.encode() + self._secret hash_output = hashlib.sha256(hash_input).digest() # Take first 4 bytes as 32-bit value F = int.from_bytes(hash_output[:4], 'big') # ISN = M + F (mod 2^32) isn = (M + F) % (2**32) return isn # Example usagegenerator = ISNGenerator()isn = generator.generate_isn("192.168.1.10", 52341, "93.184.216.34", 443)print(f"Generated ISN: {isn}") # Different connection gets completely different ISNisn2 = generator.generate_isn("192.168.1.10", 52342, "93.184.216.34", 443)print(f"Different connection ISN: {isn2}")# ISNs are uncorrelated despite similar connection tuplesIn 1994, Kevin Mitnick exploited predictable ISNs to hijack TCP connections on trusted hosts. By predicting the server's ISN, he could inject a forged ACK segment that appeared legitimate, gaining unauthorized access without seeing any response traffic. This attack drove the adoption of cryptographic ISN generation.
The sequence number field's interpretation depends on the type of TCP segment being sent. Different control flags alter how the sequence number should be understood and what sequence space it consumes.
| Segment Type | Sequence Number Meaning | Sequence Space Consumed |
|---|---|---|
| SYN (connection request) | Initial Sequence Number (ISN) | 1 sequence number |
| SYN-ACK (connection response) | Server's ISN | 1 sequence number |
| Data segment | First byte of payload | Number of payload bytes |
| FIN (connection termination) | Next byte after last data byte | 1 sequence number |
| ACK-only (no data) | Same as previous segment | 0 sequence numbers |
| RST (reset) | Expected sequence for receiver | 0 sequence numbers |
SYN Consumes a Sequence Number:
The SYN flag is considered to occupy one sequence number of space. This is crucial for acknowledgment:
Client sends: SYN, Seq=1000
Server responds: SYN-ACK, Ack=1001 (acknowledges ISN, expects 1001 next)
If SYN didn't consume a sequence number, there would be ambiguity about whether the SYN was received.
FIN Also Consumes a Sequence Number:
Similarly, FIN occupies one sequence number:
Client sends: FIN, Seq=5000 (after sending 4000 bytes of data)
Server responds: ACK, Ack=5001 (acknowledges FIN)
This ensures graceful connection termination can be reliably acknowledged.
The 32-bit sequence number creates a circular sequence space of 4,294,967,296 (4 billion) values. When the counter reaches the maximum value (2³² - 1 = 4,294,967,295), it wraps back to 0 and continues. This wraparound is normal and expected for long-lived connections or high-bandwidth transfers.
Modular Arithmetic:
Sequence number comparisons use modular (circular) arithmetic:
S1 < S2 means 0 < (S2 - S1) < 2³¹
This defines a "window" of valid sequence numbers spanning half the sequence space (2³¹ values) ahead of and behind the current position.
Wraparound Example:
Current sequence number: 4,294,967,290
Need to send 10 more bytes
Byte 1: Seq = 4,294,967,290
Byte 2: Seq = 4,294,967,291
...
Byte 6: Seq = 4,294,967,295 (maximum value)
Byte 7: Seq = 0 (wraparound!)
Byte 8: Seq = 1
Byte 9: Seq = 2
Byte 10: Seq = 3
The receiver must handle this wraparound correctly, understanding that sequence number 0 comes after 4,294,967,295 in this context.
High-Bandwidth Challenges:
Modern high-speed networks wrap around quickly:
| Link Speed | Time to Wrap 2³² Bytes | Risk Level |
|---|---|---|
| 10 Mbps | ~57 minutes | Negligible |
| 100 Mbps | ~5.7 minutes | Low |
| 1 Gbps | ~34 seconds | Moderate |
| 10 Gbps | ~3.4 seconds | High |
| 100 Gbps | ~0.34 seconds | Critical |
At 10 Gbps and above, a segment could be delayed in the network, and the sequence space could wrap around before it arrives. The receiver might accept this old segment as valid new data, corrupting the stream.
RFC 7323 defines PAWS using TCP timestamps. Each segment carries a timestamp; the receiver rejects segments with timestamps older than recently received data. This disambiguates wrapped sequence numbers, ensuring old segments can't masquerade as new data even at 100+ Gbps speeds.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
#include <stdint.h>#include <stdbool.h> /* TCP sequence number comparison using modular arithmetic * Handles wraparound correctly by treating sequence space as circular */ /* Returns true if seq1 is before seq2 in the sequence space */static inline bool seq_before(uint32_t seq1, uint32_t seq2) { /* Cast to signed for correct wraparound comparison * If (seq2 - seq1) is small positive, seq1 < seq2 * If (seq2 - seq1) is large positive (> 2^31), seq1 > seq2 * Casting to int32_t handles this automatically */ return (int32_t)(seq1 - seq2) < 0;} /* Returns true if seq1 is before or equal to seq2 */static inline bool seq_before_eq(uint32_t seq1, uint32_t seq2) { return (int32_t)(seq1 - seq2) <= 0;} /* Returns true if seq1 is after seq2 in the sequence space */static inline bool seq_after(uint32_t seq1, uint32_t seq2) { return (int32_t)(seq1 - seq2) > 0;} /* Check if sequence number is within receive window */bool seq_in_window(uint32_t seq, uint32_t window_start, uint32_t window_size) { /* Sequence is valid if: * window_start <= seq < window_start + window_size */ return seq_before_eq(window_start, seq) && seq_before(seq, window_start + window_size);} /* Example: Check wraparound behavior */void test_wraparound(void) { uint32_t seq1 = 4294967290; /* Near max value */ uint32_t seq2 = 5; /* After wraparound */ /* seq2 is AFTER seq1 despite being smaller numerically */ printf("seq_after(5, 4294967290) = %d\n", seq_after(seq2, seq1)); /* Output: 1 (true) */ /* Difference calculation works correctly */ printf("Distance: %u bytes\n", seq2 - seq1); /* Output: 11 bytes (5 - (-6) in modular arithmetic) */}Sequence numbers are not merely identifiers—they are the mechanism that transforms the unreliable IP layer into TCP's reliable byte stream. They enable three critical capabilities:
1. In-Order Delivery:
The receiver uses sequence numbers to sort arriving segments into the correct order:
Arrival order: Segment 3 (Seq=300), Segment 1 (Seq=100), Segment 2 (Seq=200)
Delivery order: Segment 1, Segment 2, Segment 3
The receiver maintains a reassembly buffer where it places segments according to their sequence numbers, delivering contiguous data to the application.
2. Gap Detection:
Missing sequence numbers indicate lost segments:
Received: Seq=100 (100 bytes), Seq=300 (100 bytes)
Missing: Seq=200 (100 bytes) — bytes 200-299 never arrived
Action: Request retransmission (via duplicate ACKs or timeout)
3. Duplicate Detection:
Retransmitted segments carry the same sequence numbers as originals:
Original: Seq=100 (100 bytes)
Retransmission: Seq=100 (100 bytes) — identical sequence numbers
Receiver: Already have bytes 100-199, discard duplicate
The Reassembly Buffer:
The receiver maintains a buffer indexed by sequence number. As segments arrive:
Buffer (RCV.NXT = 200):
[200-299: FULL] [300-399: EMPTY] [400-499: FULL] [500-599: EMPTY]
When segment Seq=300 arrives:
[200-299: FULL] [300-399: FULL] [400-499: FULL] [500-599: EMPTY]
Now bytes 200-499 are contiguous:
- Deliver 200-499 to application
- Update RCV.NXT = 500
- Send ACK with Ack=500
This mechanism allows TCP to handle arbitrary network reordering transparently.
Standard TCP ACKs are cumulative—they acknowledge all bytes up to a point. SACK (RFC 2018) extends this by reporting non-contiguous blocks that have been received. This allows the sender to retransmit only the actual missing segments rather than everything after the gap.
Sequence numbers and acknowledgment numbers are intimately connected—they are two sides of the same reliability mechanism. While the sequence number labels outgoing data, the acknowledgment number confirms received data.
The Fundamental Relationship:
Acknowledgment Number = Last Contiguous Sequence Number Received + 1
= Next Expected Sequence Number
When the receiver sends ACK = 500, it's saying:
Full-Duplex Symmetry:
Both hosts simultaneously act as senders and receivers. Each direction has independent sequence numbers:
A → B: Seq from A's sequence space, Ack references B's sequence space
B → A: Seq from B's sequence space, Ack references A's sequence space
Example Exchange:
| Segment | Direction | Seq | Ack | Interpretation |
|---|---|---|---|---|
| 1 | A → B | 1000 | - | A starts sending from 1000 |
| 2 | B → A | 5000 | 1100 | B sends from 5000, ACKs A's 1000-1099 |
| 3 | A → B | 1100 | 5050 | A continues from 1100, ACKs B's 5000-5049 |
| 4 | B → A | 5050 | 1200 | B continues from 5050, ACKs A's 1100-1199 |
When both directions have data to send, TCP 'piggybacks' acknowledgments onto data segments rather than sending separate ACK-only segments. This reduces overhead: a single segment carries data in one direction and acknowledgment for the reverse direction.
We've conducted a comprehensive exploration of TCP sequence numbers—the 32-bit field that transforms unreliable IP datagrams into a reliable, ordered byte stream.
What's Next:
Sequence numbers tell the receiver what data is being sent; acknowledgment numbers tell the sender what data has been received. The next page explores the acknowledgment number field—the complementary half of TCP's reliability mechanism that enables cumulative acknowledgment and drives retransmission decisions.
You now possess comprehensive understanding of TCP sequence numbers: their position in the header, byte-oriented semantics, secure ISN generation, segment type variations, wraparound handling, and their role as the foundation of reliable delivery. This prepares you for understanding how acknowledgment numbers complete the reliability picture.