Networking Fundamentals

Ch.19: Anatomy of a TCP Segment

By Ayush Arora11 min read

Inspired by: YouTube

We already dissected the Anatomy of a UDP Datagram down to its 4 fields and 8-byte header. We've also covered TCP's Sequence and Acknowledgment Numbers in isolation. In this post, we zoom out and examine the complete TCP segment header, field by field, exactly as defined in the official RFC.

A TCP segment, just like a UDP datagram, travels across the network carried inside an IP packet. But unlike UDP's minimal 8-byte header, TCP carries significantly more metadata, because it's responsible for reliability, ordering, flow control, and congestion control.


RFC 793 / RFC 9293: The Source of Truth

The header layout below is modeled directly on the official TCP RFC, which defines the segment format as a 32-bit-wide grid (4 bytes per row). Although diagrams represent it as rows and columns for readability, on the wire it is always a continuous stream of bytes, source port, then destination port, then sequence number, and so on, one after another.

1st Byte (0-7)2nd Byte (8-15)3rd Byte (16-23)4th Byte (24-31)
012345678910111213141516171819202122232425262728293031
Source PortDestination Port
Sequence Number
Acknowledgment Number
Data OffsetReservedCWRECEURGACKPSHRSTSYNFINWindow Size
ChecksumUrgent Pointer
Options (Optional, if data offset > 5, padding (0s) is added at the end if required)
Data

Click any field to highlight the exact bits it occupies in the header.

Everything from Source Port through Options is the TCP header, the metadata needed to make TCP function. The Data field is the actual payload (an HTTP request, an SSH command, an SMTP message) that the higher-layer protocol hands off to TCP. TCP acts purely as a vehicle: it carries this payload while enforcing reliability and ordering on top of it.


Source Port and Destination Port (2 Bytes Each)

Identical in concept to UDP: the Source Port identifies the application process that initiated the request (say, a React app running on the client host), and the Destination Port identifies the application process the segment is headed to (say, an Express server on port 8000). Since each field is 16 bits wide, both range from 0 to 65535.


Sequence Number and Acknowledgment Number (4 Bytes Each)

We covered both of these fields in full detail in the previous post, so we won't repeat that here. What's worth noting on the header level:


Data Offset (4 Bits) and Reserved (4 Bits)

The Data Offset field tells the receiver exactly where the header ends and the Data payload begins.

The Reserved field is simply 4 bits set aside for future use (typically 0), giving the protocol room to grow without breaking backward compatibility, for example, to accommodate additional flags later.


The 8 Control Flags

Each flag is a single bit: 0 means disabled, 1 means enabled. Multiple flags can be set simultaneously on the same segment (a SYN-ACK segment, for instance, has both SYN and ACK set to 1).

SYN

Set to 1 on the very first segment of the 3-way handshake. It tells the receiver, "I want to synchronize, here's my Initial Sequence Number, and I'll expect subsequent sequence numbers to follow from it." We covered this in detail in our 3-way handshake post.

ACK

Whenever this flag is 1, the segment is required to carry a valid Acknowledgment Number. If the flag is 0, the Acknowledgment Number field is ignored entirely, sender and receiver simply don't consider it. This is why TCP is described as an "acknowledgment-oriented" protocol: virtually every segment exchanged (after the initial SYN) carries an ACK.

FIN

Set to 1 when a host wants to gracefully close its side of the connection ("I've sent everything I need to send. I want to close."). Unlike SYN or ACK, FIN doesn't unlock any additional header field, it's purely a signal. We covered the full graceful teardown sequence in our 4-way termination post.

RST

Set to 1 for an abrupt, non-graceful termination, a "mayday" signal used when something has gone wrong (the server crashed, the port is closed, or a segment arrived for a connection that no longer exists in the state table). Unlike FIN's mutual, negotiated close, RST forces an immediate teardown with no handshake required.

PSH (Push)

Under normal conditions, an OS buffers incoming TCP segments in kernel memory for a short window, batching several segments together before handing them up to the application (say, Express), this is more resource-efficient than delivering every single segment individually. The PSH flag overrides this optimization: when set to 1, it tells the receiving OS, "Don't buffer this, push it up to the application immediately."

ECE (ECN-Echo) and CWR (Congestion Window Reduced)

These two flags work together as a congestion feedback loop between sender and receiver:

Note: ECE, CWR, the Congestion Window, and ECN (Explicit Congestion Notification) are all part of TCP's broader congestion control mechanism. We'll cover this entire system in depth in a dedicated future post; for now, it's enough to understand what these two flags signal.

URG and the Urgent Pointer

The URG flag, when set to 1, tells the receiver to interpret the Urgent Pointer field (covered next). A classic example: in a Telnet session, if a remote command starts producing runaway output and you hit Ctrl+C, that interrupt byte travels over the network flagged as urgent, telling the receiver, "process this byte immediately, don't wait for the rest of the buffered stream."

In practice, URG is rarely used in modern systems and different operating systems (Windows vs. Linux) have historically interpreted it inconsistently, which has caused real-world confusion. In fact, the RFC history itself reflects this:

RFC 793 (1981) originally stated the Urgent Pointer "points to the sequence number of the octet following the urgent data." RFC 1011 (1987) later clarified that this was actually wrong: the Urgent Pointer points to the last octet of the urgent data itself, not the first octet after it. More recently, the modern TCP specification RFC 9293 explicitly recommends that new applications should not use the TCP urgent mechanism at all (SHLD-13), even though implementations are still required to support it for backward compatibility (MUST-30).


Window Size (2 Bytes)

The Window Size field tells the other side how much data the sender is currently able to receive and buffer, its receive capacity. If a host has, say, 1 GB of data to send, it can't blast it all in one continuous stream; it needs to know how much the receiver can currently handle. We'll cover exactly how this drives TCP Flow Control in a dedicated future post.


Checksum (2 Bytes)

Functions identically to the UDP checksum we covered in the UDP anatomy post: the sender computes a value over the header and payload and embeds it here. The receiver recalculates the same checksum on arrival, if the two don't match, the segment is considered corrupted and is silently dropped.


Options (0 to 40 Bytes)

Beyond the 5 mandatory header rows (20 bytes), TCP allows optional, dynamic headers to extend functionality. This field can grow up to 10 additional rows (40 bytes), which is exactly why Data Offset can range higher than 5. A few of the more relevant options:


Data (Variable Length)

The final field carries the actual application payload, an HTTP request, an SSH session byte stream, an FTP file chunk, an SMTP email, or any other higher-layer protocol's data. TCP itself doesn't care what's inside; it just reliably transports it.


Calculating TCP Header Size: 20 to 60 Bytes

Summing up the byte weight of every mandatory field:

FieldSize
Source Port2 bytes
Destination Port2 bytes
Sequence Number4 bytes
Acknowledgment Number4 bytes
Data Offset + Reserved + Flags2 bytes
Window Size2 bytes
Checksum2 bytes
Urgent Pointer2 bytes
Required Header Total20 bytes

This matches the 5 mandatory rows we saw earlier (5 × 4 bytes = 20 bytes), and is why the minimum Data Offset value is always 5.

On top of this, the Options field can add up to 40 bytes (10 more rows). That gives us:

Maximum TCP Header Size = 20 bytes (Required) + 40 bytes (Optional) = 60 bytes

This is a frequently asked interview detail, and it's the exact number we'll reuse when we calculate the Maximum Segment Size (MSS) in a future post, for most practical purposes though, you can treat 20 bytes as the standard TCP header size, since Options are only present when a specific feature (like SACK or Window Scaling) requires them.


Summary