Your application might want to send a 50KB message in one go. But the physical network cannot carry arbitrary-sized packets โ it has a size limit called the MTU. When an IP datagram is too big, IP fragments it into smaller pieces. This page explains how that works, why it can be a problem, and how modern TCP avoids it.
The MTU is the maximum size (in bytes) of a single data frame that a data link layer can carry. Think of it as the size of a truck. You can only load as much as the truck can carry in one trip.
The Ethernet MTU (1,500 bytes) is by far the most important one in practice because most internet traffic passes through Ethernet at some point. This means IP datagrams larger than 1,500 bytes will need to be fragmented.
The Path MTU is the minimum MTU across all network links between source and destination. A packet traveling from your laptop through WiFi (2304 MTU) then through Ethernet (1500 MTU) has a Path MTU of 1500 โ the smallest link in the chain.
When an IP datagram is larger than the MTU, the IP layer at the sender (or at a router) splits it into smaller fragments. Each fragment is itself a valid IP datagram with its own IP header. The fragments are reassembled only at the final destination โ not at intermediate routers.
Key fields in the IP header used for fragmentation:
- Fragment Offset: tells the receiver where in the original datagram this fragment belongs (measured in 8-byte units)
- More Fragments (MF) flag: set to 1 on all fragments except the last one
- Identification field: all fragments of the same original datagram share the same ID, so the receiver knows which fragments belong together
IP fragmentation happens transparently โ the application and TCP/UDP do not even know it happened. But it has a serious problem:
IP does not retransmit. A datagram can be reassembled at the destination only if ALL fragments arrive.
If even one fragment is lost in transit, the entire original datagram is useless and must be discarded. There is no way to request just the missing fragment.
The consequences depend on which transport protocol is used:
If any fragment is lost, the entire UDP datagram is dropped. The application never receives it. UDP has no way to recover. This leads to significant data loss rates on networks prone to packet loss.
TCP detects the loss (via timeout or duplicate ACK) and retransmits the data. But fragmentation still causes degraded throughput because the entire TCP segment must be retransmitted, not just the lost fragment.
Modern TCP implementations use Path MTU Discovery (PMTUD) to figure out the Path MTU before sending large segments, so that IP is never asked to fragment.
UDP has no PMTUD mechanism. UDP-based applications must either keep their messages small (under 512 bytes is a common safe choice) or implement their own fragmentation handling at the application level.
Linux lets a socket enable Path MTU Discovery explicitly using IP_MTU_DISCOVER socket option. After connecting, you can query the discovered Path MTU with IP_MTU.
/* path_mtu_discover.c
* Enables Path MTU Discovery on a TCP socket and
* reads the discovered Path MTU after connecting.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
if (argc != 3) {
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(EXIT_FAILURE);
}
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) { perror("socket"); exit(EXIT_FAILURE); }
/*
* IP_MTU_DISCOVER controls PMTUD behavior:
* IP_PMTUDISC_DO = always set DF bit, perform PMTUD
* IP_PMTUDISC_DONT = never set DF bit, allow fragmentation
* IP_PMTUDISC_WANT = set DF bit only if PMTUD is globally enabled
*/
int pmtud = IP_PMTUDISC_DO;
if (setsockopt(sockfd, IPPROTO_IP, IP_MTU_DISCOVER,
&pmtud, sizeof(pmtud)) == -1) {
perror("setsockopt IP_MTU_DISCOVER");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Path MTU Discovery enabled (DF bit will be set)\n");
/* Connect to remote host */
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
if (inet_pton(AF_INET, argv[1], &addr.sin_addr) != 1) {
fprintf(stderr, "Invalid IP address\n");
exit(EXIT_FAILURE);
}
if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
perror("connect");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Connected to %s:%s\n", argv[1], argv[2]);
/*
* After connecting, query the kernel for the discovered Path MTU.
* This is the maximum segment size we can use without triggering
* IP fragmentation on this path.
*/
int path_mtu;
socklen_t optlen = sizeof(path_mtu);
if (getsockopt(sockfd, IPPROTO_IP, IP_MTU, &path_mtu, &optlen) == -1) {
perror("getsockopt IP_MTU");
} else {
printf("Discovered Path MTU: %d bytes\n", path_mtu);
printf("Max safe payload per TCP segment: %d bytes\n",
path_mtu - 40); /* 20 IP header + 20 TCP header */
}
close(sockfd);
return 0;
}
/* Compile and run:
gcc -o path_mtu path_mtu_discover.c
./path_mtu 93.184.216.34 80 (example.com port 80)
Expected output (Ethernet path):
Path MTU Discovery enabled (DF bit will be set)
Connected to 93.184.216.34:80
Discovered Path MTU: 1500 bytes
Max safe payload per TCP segment: 1460 bytes
*/
For UDP, the best practice to avoid fragmentation is to keep datagrams small. The safe limit for UDP payload is typically 512 bytes (DNS uses this). If you need larger messages, consider implementing application-level segmentation.
/* udp_size_check.c
* Demonstrates checking if a UDP payload exceeds safe MTU limits.
* UDP has no PMTUD, so the application must be careful.
*/
#include <stdio.h>
#include <string.h>
#define ETHERNET_MTU 1500
#define IP_HEADER_SIZE 20
#define UDP_HEADER_SIZE 8
#define MAX_SAFE_UDP_PAYLOAD (ETHERNET_MTU - IP_HEADER_SIZE - UDP_HEADER_SIZE)
/* MAX_SAFE_UDP_PAYLOAD = 1500 - 20 - 8 = 1472 bytes */
#define CONSERVATIVE_UDP_LIMIT 512 /* used by DNS, avoids fragmentation risk */
void check_udp_payload_size(size_t payload_size)
{
printf("Payload size: %zu bytes\n", payload_size);
if (payload_size <= CONSERVATIVE_UDP_LIMIT) {
printf("Status: SAFE โ below conservative 512-byte limit\n");
} else if (payload_size <= MAX_SAFE_UDP_PAYLOAD) {
printf("Status: OK for Ethernet โ but may fragment on smaller MTU paths\n");
} else {
printf("Status: WARNING โ exceeds Ethernet MTU, WILL cause IP fragmentation!\n");
printf("Consider splitting into multiple UDP datagrams of <= %d bytes each\n",
MAX_SAFE_UDP_PAYLOAD);
}
printf("\n");
}
int main(void)
{
check_udp_payload_size(200); /* DNS query โ safe */
check_udp_payload_size(1000); /* OK for Ethernet, risky on some paths */
check_udp_payload_size(2000); /* Will cause fragmentation */
check_udp_payload_size(65000); /* Definitely fragmented */
return 0;
}
| Parameter | IPv4 | IPv6 |
|---|---|---|
| Max datagram size | 65,535 bytes | 65,575 bytes (40B header + 65,535B data) |
| Jumbogram support | No | Yes (optional extension) |
| Min reassembly buffer | 576 bytes | 1,500 bytes |
| Typical Ethernet MTU | 1,500 bytes (both IPv4 and IPv6) | |
| Who can fragment? | Sender or routers | Source host only (routers cannot fragment) |
| Typical TCP MSS | 1,460 bytes (1,500 MTU โ 20 IP hdr โ 20 TCP hdr) | |
Next: IP Addresses & Subnetting โ โ Part 2: IP Network Layer
