Why Does TCP Need Sequence Numbers?
Imagine you send a 3-page letter by breaking it into 3 separate envelopes. If they arrive in the order 2, 3, 1 โ you need page numbers to reassemble them correctly. TCP faces the same problem: segments can arrive out of order, get duplicated, or get lost. Sequence numbers solve all three problems.
TCP assigns a logical sequence number to every byte it sends. The receiver uses these numbers to reassemble the stream in the correct order and deliver it to the application as a clean, in-order byte stream.
Every byte in a TCP connection has a unique sequence number. These numbers are tracked within each data stream separately (there are two streams per TCP connection โ one in each direction).
Each TCP segment carries the sequence number of its first byte. For example, if a segment starts with byte 1000 and carries 500 bytes of data, the sequence number in the TCP header is 1000, and the segment covers bytes 1000โ1499.
1
2
3
4
5
7
8
9
10
11
IP does not guarantee segment ordering. Two segments sent in order may arrive in reverse order. The receiving TCP buffers out-of-order segments and delivers them to the application only after filling any gaps โ always in the correct sequence.
If a retransmission arrives after the original was already delivered, the receiver sees a duplicate sequence number. It discards the duplicate silently.
If seq=1001 arrives but seq=501 has not yet arrived, the receiver knows segment 501 is missing. It can hold seq=1001 in its buffer until 501 arrives (or use SACK to tell the sender exactly which segments are missing).
TCP does not start counting bytes from 0 or 1. Each connection starts with a randomly chosen Initial Sequence Number (ISN). Two important reasons:
If a connection between the same two ports is restarted, segments from the old connection still in the network could be mistaken for new-connection data. A different ISN makes old segments irrelevant because their sequence numbers fall outside the new connection’s range.
If an attacker could predict the ISN, they could inject fake segments into a connection. The ISN is generated using an algorithm that makes it hard to guess, providing basic protection against TCP session hijacking.
The sequence number is a 32-bit unsigned integer. After reaching the maximum value (2ยณยฒ โ 1 = 4,294,967,295), it wraps around to 0 and continues. On very high-speed links, wrap-around can happen fast, which is why TCP options like Timestamps (RFC 1323) exist to handle this safely.
/* tcp_seq_info.c - read TCP sequence state after connection */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
int main(void)
{
int sockfd;
struct sockaddr_in srv;
struct tcp_info ti;
socklen_t tilen = sizeof(ti);
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) { perror("socket"); exit(EXIT_FAILURE); }
memset(&srv, 0, sizeof(srv));
srv.sin_family = AF_INET;
srv.sin_port = htons(80); /* connect to a real HTTP server */
srv.sin_addr.s_addr = inet_addr("93.184.216.34"); /* example.com */
if (connect(sockfd, (struct sockaddr *)&srv, sizeof(srv)) == 0) {
printf("Connected!\n");
/*
* TCP_INFO gives us the kernel's view of this connection:
* - tcpi_snd_mss: Maximum Segment Size for sending
* - tcpi_rcv_mss: Maximum Segment Size for receiving
* - tcpi_last_data_sent: ms since last data was sent
* - tcpi_rtt: smoothed RTT in microseconds
* - tcpi_snd_cwnd: current congestion window (segments)
* - tcpi_snd_ssthresh: slow-start threshold
*/
if (getsockopt(sockfd, IPPROTO_TCP, TCP_INFO, &ti, &tilen) == 0) {
printf("Send MSS: %u bytes\n", ti.tcpi_snd_mss);
printf("Recv MSS: %u bytes\n", ti.tcpi_rcv_mss);
printf("RTT (smoothed): %u microseconds\n", ti.tcpi_rtt);
printf("RTT variance: %u microseconds\n", ti.tcpi_rttvar);
printf("Congestion window:%u segments\n", ti.tcpi_snd_cwnd);
printf("Retransmits: %u\n", ti.tcpi_retransmits);
} else {
perror("getsockopt TCP_INFO");
}
} else {
perror("connect");
}
close(sockfd);
return 0;
}
/*
* Observing sequence numbers with tcpdump (shell command, not C)
* Run this in a terminal alongside your program:
*
* sudo tcpdump -i lo -n -S tcp port 8080
*
* The -S flag shows ABSOLUTE sequence numbers (the actual ISN-based values)
* Without -S, tcpdump shows RELATIVE sequence numbers (starting from 0 for readability)
*
* Example output:
* 13:42:01 IP 127.0.0.1.45678 > 127.0.0.1.8080: Flags [S], seq 3274019832
* 13:42:01 IP 127.0.0.1.8080 > 127.0.0.1.45678: Flags [S.], seq 8921047213, ack 3274019833
* 13:42:01 IP 127.0.0.1.45678 > 127.0.0.1.8080: Flags [.], ack 8921047214
*
* Flags: S=SYN, .=ACK, P=PSH (data), F=FIN, R=RST
*/
A sequence number identifies the position of the first byte of data in that segment within the overall byte stream. If a segment has seq=1000 and carries 500 bytes, it covers the stream positions 1000โ1499. The next segment should have seq=1500.
Starting at a random ISN avoids two problems: (1) old segments from a previous incarnation of the same connection (same ports, same hosts) being confused with new connection data, and (2) an attacker predicting the sequence number and injecting forged segments (session hijacking).
The receiving TCP buffers out-of-order segments (using the sequence numbers to know where they belong). It waits until the gap is filled โ either by receiving the missing segment or via retransmission. Only after filling the gap does it deliver data to the application in correct order.
The receiver tracks the next expected sequence number. If a segment arrives with a sequence number below this value (meaning it was already received and acknowledged), the receiver discards it and sends an ACK for the sequence number it actually expects next. The duplicate data is never passed to the application.
The sequence number is 32 bits, so it can hold values from 0 to 4,294,967,295 (about 4 GB). After reaching the max, it wraps back to 0. On 10 Gbps links this can happen in under 4 seconds. TCP handles wrap-around through the Timestamps option (RFC 1323), which adds a timestamp to segments for PAWS (Protection Against Wrapped Sequence numbers).
The ACK number is the sequence number of the next byte the receiver expects. If the receiver has successfully received bytes 1โ1000, it sends ACK=1001. This is a cumulative ACK โ it implicitly confirms all bytes up to 1000 were received correctly.
