Networking Fundamentals

Ch.20: MTU, MSS, and Path MTU Discovery

By Ayush Arora11 min read

Inspired by: YouTube

In the previous post, we saw that the TCP Options field can carry an MSS value negotiated during the handshake, but we deferred explaining what MSS actually is. In this post we cover three tightly related ideas: MTU (how big a single IP packet can be), MSS (how much TCP payload can fit inside that limit), and Path MTU Discovery (how a sender figures out the smallest MTU across every hop on the way to its destination).


MTU: Maximum Transmission Unit

The MTU is the largest IP packet a given link can carry without fragmentation. Every IP packet we've seen while covering routing consists of an IP header (source IP, destination IP, and other fields) wrapping a payload, which for our purposes is a TCP segment.

MTU (Maximum Transmission Unit)
Ethernet Frame
IP Headers
TCP Headers
Data/Payload
14 bytes
20 bytes
20 bytes
MSS (Maximum Segment Size)

MTU covers everything except the Ethernet frame; MSS covers only the TCP payload.

The diagram above shows how everything fits together: the Ethernet frame (14 bytes) sits outside the picture entirely, the IP header (20 bytes) and TCP header (20 bytes) come next, and finally the Data/Payload. The MTU spans the IP header, TCP header, and data combined, while the MSS covers only the data portion, which is exactly what the next two sections walk through.

A few things worth nailing down:

Exceeding the MTU forces the network stack to fragment the packet, and fragmentation, as we'll see next, always costs you extra bytes.


IP Fragmentation

Say your network's MTU is 1500 bytes, but the original IP packet you need to send is 1600 bytes: a 20-byte IP header plus a 1580-byte TCP segment. Note that the 20-byte IP header only ever carries the source and destination IP addresses, ports are a transport-layer concept and never appear there. The 1580-byte "payload" is really the entire TCP segment: its own header (which is where the source/destination ports actually live) plus the real application data. From IP's point of view, that whole TCP segment is just an opaque blob it's carrying, it doesn't look inside it at all. Since 1600 exceeds the 1500 MTU, the TCP/IP stack fragments the packet into two independent IP packets.

Add up the two fragments: 1500 + 120 = 1620 bytes, sent over the wire to deliver data that was originally only 1600 bytes. That extra 20 bytes is the duplicated IP header, required because each fragment must be independently routable and therefore needs its own complete header.

This is exactly why fragmentation should be avoided wherever possible: it inflates the amount of data transferred over the network for no benefit. It also has downsides beyond overhead: fragmented traffic is more complex to reassemble correctly and is generally considered less secure. (How a receiver actually detects and reassembles fragments is a detail encoded in the IP header itself, which we'll cover when we dissect IP packet anatomy.)

Teardrop attack: a real-world example of fragmentation's security risk, from around 1997. Each IP fragment carries an offset saying where its data belongs in the original packet, normally fragments line up back-to-back with no overlap. Teardrop sent fragments with deliberately overlapping offsets, and vulnerable OS kernels (Windows 95/NT, older Linux) did the reassembly math wrong on that overlap, underflowing into a huge, invalid length and crashing the machine, a Blue Screen of Death or kernel panic, from just a couple of malformed packets. It was a pure denial-of-service: no data stolen, no code run, just a bad assumption in the reassembly code that nobody validated. Modern systems patch against it by rejecting overlapping fragments outright.


MSS: Maximum Segment Size

MSS solves the fragmentation problem from the TCP side, by making sure segments are built to fit inside the MTU in the first place.

Going back to the MTU/MSS diagram above: with a 1500-byte MTU, a 20-byte IP header, and a 20-byte TCP header, the Data/Payload block, the MSS, works out to:

MSS = MTU − IP Header − TCP Header

With the standard values (1500 MTU, 20-byte IP header, 20-byte TCP header at minimum): 1500 − 20 − 20 = 1460 bytes. This is why 1460 is the standard Ethernet MSS you'll see quoted almost everywhere.

How MSS is exchanged

MSS is advertised during the TCP 3-way handshake, carried inside the segment's Options field (the same field we saw in the TCP segment anatomy post). There's no dedicated fixed header slot for it; it rides along in Options alongside the initial sequence number and window size.

Why MSS prevents fragmentation

Once each side knows the other's MSS, TCP chunks outgoing data into segments that respect it. Say a client needs to send 3000 bytes of data and it knows the server's MSS is 1460:

TCP builds two full 1460-byte segments (1460 + 1460 = 2920 bytes) and a final 80-byte segment for the remainder. Once the TCP header (20 bytes) and IP header (20 bytes) are added to a 1460-byte data segment, the resulting IP packet is exactly 1460 + 20 + 20 = 1500 bytes, which matches the server's MTU precisely. No fragmentation needed.

If the client instead ignored MSS and built a 1500-byte data segment, the final IP packet would come out to 1500 + 20 + 20 = 1540 bytes, exceeding the 1500 MTU and forcing fragmentation into two packets, exactly the wasteful scenario from the previous section. This is the entire point of MSS: it lets TCP construct segments that, once wrapped in IP and TCP headers, land exactly at the MTU instead of spilling over it.


PMTUD: Path MTU Discovery

MSS solves the problem between a client and a server directly, but data doesn't travel from client to server in a single hop. It passes through multiple routers, and each router along the path can have its own MTU. If any router in the middle has a smaller MTU than what the client assumed, fragmentation still happens, just further down the path.

The problem

If the client sends a 1500-byte IP packet, Router 1 (MTU 1420) fragments it into a 1420-byte piece and a smaller remainder. Router 2 (MTU 512) then finds that even the 1420-byte fragment exceeds its MTU, and fragments it again. A single packet the client sent ends up arriving at the server broken into three separate fragments, having crossed only two intermediate routers.

The fix isn't to guess: it's to discover the smallest MTU along the entire path (the "Path MTU") up front, and size every packet to fit it from the start.

The mechanism: the Don't Fragment (DF) flag

The IP header contains a single-bit flag called DF (Don't Fragment). When a sender sets DF = 1 on a packet, it's telling every router along the path: "you are not allowed to fragment this packet." If a router's MTU is smaller than the packet and DF is set, the router cannot fragment it, so it drops the packet instead and notifies the sender via an ICMP "Fragmentation Needed" message containing its own MTU.

Walking through it:

  1. The client starts with its own standard MTU (1500 bytes) and sets DF = 1.
  2. Router 1's MTU (1420) is smaller than the packet, so it drops the packet and sends back an ICMP message reporting its MTU of 1420.
  3. The client rebuilds the packet at exactly 1420 bytes and resends. Router 1 now forwards it without issue, but Router 2's MTU (512) is smaller still, so Router 2 drops it and reports its MTU of 512.
  4. The client rebuilds the packet at 512 bytes. This time it passes through both routers cleanly and reaches the server without a single fragmentation along the way.

From this point on, the client knows the Path MTU is 512 bytes and sizes all further packets to that value, avoiding fragmentation for the rest of the connection.

This is also why fragmentation is treated as something to actively avoid: it's not just wasted bytes, IP fragmentation is also considered a weaker point from a security standpoint, since reassembly logic has historically been a source of vulnerabilities. Knowing the Path MTU up front sidesteps all of that.


Summary