Loading learning content...
Before a datagram is fragmented, network engineers often need to predict the outcome: How many fragments will this datagram produce? This isn't merely academic curiosity—it directly impacts:
This page develops the mathematical formulas for calculating fragment counts and explores scenarios where multiple fragmentation events occur along a path.
By the end of this page, you will understand: (1) The ceiling function formula for fragment count calculation, (2) How IP header size affects fragment counts, (3) Multi-hop fragmentation and cumulative fragment counts, (4) Probability implications of fragmentation, and (5) Practical techniques for estimating fragmentation in complex networks.
Calculating the number of fragments requires determining how many maximum-sized pieces the original data will produce, plus any remainder.
The Fundamental Formula:
Number of Fragments = ⌈ Original_Data_Size / Max_Fragment_Data ⌉
Where:
Why Ceiling Function?
If the original data doesn't divide evenly into max-sized fragments, we need an additional fragment for the remainder. The ceiling function automatically accounts for this.
Example:
123456789101112131415161718192021222324252627282930313233343536373839404142434445
import math def calculate_fragment_count(original_data_size, mtu, header_size=20): """ Calculate the number of fragments produced. Args: original_data_size: Size of data in original datagram (bytes) mtu: Maximum Transmission Unit (bytes) header_size: IP header size (default 20 bytes) Returns: int: Number of fragments """ # Calculate maximum data per fragment (8-byte aligned) max_fragment_data = ((mtu - header_size) // 8) * 8 # Calculate number of fragments using ceiling division num_fragments = math.ceil(original_data_size / max_fragment_data) return num_fragments # Examples with different datagram sizesprint("Fragment Count Examples (MTU=1500, Header=20 bytes):")print(f"Max fragment data: {((1500-20)//8)*8} bytes\n") test_sizes = [1000, 1480, 1481, 2960, 4440, 5000, 10000, 65515] for size in test_sizes: count = calculate_fragment_count(size, 1500) exact = size / 1480 print(f"Data: {size:5d} bytes → {exact:7.3f} → ⌈{exact:.3f}⌉ = {count} fragments") # Output:# Fragment Count Examples (MTU=1500, Header=20 bytes):# Max fragment data: 1480 bytes## Data: 1000 bytes → 0.676 → ⌈0.676⌉ = 1 fragments# Data: 1480 bytes → 1.000 → ⌈1.000⌉ = 1 fragments# Data: 1481 bytes → 1.001 → ⌈1.001⌉ = 2 fragments ← Note: 1 byte over!# Data: 2960 bytes → 2.000 → ⌈2.000⌉ = 2 fragments# Data: 4440 bytes → 3.000 → ⌈3.000⌉ = 3 fragments# Data: 5000 bytes → 3.378 → ⌈3.378⌉ = 4 fragments# Data: 10000 bytes → 6.757 → ⌈6.757⌉ = 7 fragments# Data: 65515 bytes → 44.267 → ⌈44.267⌉ = 45 fragments ← Maximum IPv4 dataNotice that 1,480 bytes produces 1 fragment, but 1,481 bytes produces 2 fragments. A single extra byte doubles the fragment count! This cliff effect has significant implications for protocol design and MTU optimization.
The ceiling function isn't always convenient for manual calculation. Here are equivalent formulas that may be easier to compute:
Method 1: Integer Division with Remainder Check
full_fragments = data_size // max_fragment_data
remainder = data_size % max_fragment_data
fragment_count = full_fragments + (1 if remainder > 0 else 0)
Method 2: Ceiling via Floor
fragment_count = (data_size + max_fragment_data - 1) // max_fragment_data
This is a common trick: adding (divisor - 1) before integer division achieves ceiling behavior.
Method 3: Explicit Ceiling Calculation
if data_size % max_fragment_data == 0:
fragment_count = data_size // max_fragment_data
else:
fragment_count = data_size // max_fragment_data + 1
12345678910111213141516171819202122232425262728293031323334353637
import math def method_ceiling(data, max_frag): """Using math.ceil""" return math.ceil(data / max_frag) def method_remainder(data, max_frag): """Using integer division and remainder""" full = data // max_frag remainder = data % max_frag return full + (1 if remainder > 0 else 0) def method_floor_trick(data, max_frag): """Ceiling via floor trick""" return (data + max_frag - 1) // max_frag # Verify all methods produce identical resultstest_cases = [ (1000, 1480), (1480, 1480), (1481, 1480), (5000, 1480), (65515, 1480),] print("Verification: All methods produce identical results")print("-" * 60) for data, max_frag in test_cases: r1 = method_ceiling(data, max_frag) r2 = method_remainder(data, max_frag) r3 = method_floor_trick(data, max_frag) assert r1 == r2 == r3, "Methods disagree!" print(f"Data={data:5d}, MaxFrag={max_frag} → {r1} fragments ✓") print("\nAll methods verified identical. Use whichever is most convenient.")For manual calculations on exams: use (data + max_frag - 1) ÷ max_frag with integer division. This avoids decimals entirely. Example: (5000 + 1480 - 1) ÷ 1480 = 6479 ÷ 1480 = 4 (integer division).
IP options complicate fragment count calculations because they affect header sizes differently for different fragments.
Recall: Option Handling During Fragmentation
Asymmetric Header Sizes:
When non-copied options exist:
This creates asymmetric max data capacities:
| Fragment | Header Size | Max Data (MTU=1500) | Reduction |
|---|---|---|---|
| Without options | 20 bytes | 1480 bytes | Baseline |
| With Record Route | 60 bytes* | 1440 bytes | -40 bytes |
*Header = 20 (base) + 39 (option) + 1 (padding to 4-byte boundary) = 60 bytes
Impact on Fragment Count:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
import math def fragment_count_with_options(data_size, mtu, first_header, rest_header): """ Calculate fragments when first fragment has different header size. Args: data_size: Original data size mtu: Maximum Transmission Unit first_header: Header size for first fragment rest_header: Header size for subsequent fragments """ # First fragment capacity first_capacity = ((mtu - first_header) // 8) * 8 # Subsequent fragment capacity rest_capacity = ((mtu - rest_header) // 8) * 8 if data_size <= first_capacity: return 1 # Data remaining after first fragment remaining = data_size - first_capacity # Fragments needed for remaining data rest_fragments = math.ceil(remaining / rest_capacity) return 1 + rest_fragments # Compare: with and without optionsdata_size = 10000mtu = 1500 # Without optionsno_opts = math.ceil(data_size / 1480) # With copied options (60-byte header for all)copied = math.ceil(data_size / 1440) # With non-copied options (60-byte first, 20-byte rest)non_copied = fragment_count_with_options(data_size, mtu, 60, 20) print(f"Fragmenting {data_size} bytes over MTU {mtu}:")print(f" Without options: {no_opts} fragments")print(f" With copied options: {copied} fragments (all have 60-byte header)")print(f" With non-copied opts: {non_copied} fragments (first=60, rest=20)") print("\nDetailed breakdown for non-copied options:")first_data = ((1500 - 60) // 8) * 8 # 1440 bytesrest_data = ((1500 - 20) // 8) * 8 # 1480 bytesremaining = data_size - first_datarest_frags = math.ceil(remaining / rest_data)print(f" Fragment 1: {first_data} bytes (larger header)")print(f" Remaining: {remaining} bytes")print(f" Fragments 2+: {rest_frags} × {rest_data} bytes max")print(f" Total: 1 + {rest_frags} = {1 + rest_frags} fragments")IP options are rarely used in modern networks, so the standard 20-byte header assumption usually holds. However, for comprehensive analysis of packet captures or legacy systems, accounting for options is essential.
When datagrams traverse paths with decreasing MTUs, fragmentation can occur at multiple points. This cascading fragmentation significantly increases the total fragment count.
Cascading Fragmentation Scenario:
Source → Router A → Router B → Router C → Destination
MTU 1500 MTU 1000 MTU 576
A large datagram may be fragmented at Router A, then each of those fragments may be re-fragmented at Router B, and again at Router C.
Key Insight: Fragmentation is Multiplicative
If Router A creates N fragments, and Router B fragments each of those into M fragments, the total becomes N × M fragments (in the worst case).
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
import math def max_fragment_data(mtu, header=20): """Calculate max data per fragment for given MTU.""" return ((mtu - header) // 8) * 8 def fragment_single_hop(data_size, mtu): """Fragment data at a single hop, return list of fragment data sizes.""" max_data = max_fragment_data(mtu) fragments = [] remaining = data_size while remaining > 0: frag_size = min(remaining, max_data) # Non-last fragments must be 8-byte aligned if remaining > max_data: frag_size = max_data # Already aligned fragments.append(frag_size) remaining -= frag_size return fragments def cascade_fragmentation(original_data, mtu_path): """ Simulate fragmentation across a path with decreasing MTUs. Args: original_data: Original data size mtu_path: List of MTUs along the path Returns: List of final fragment sizes """ # Start with original datagram current_fragments = [original_data] for hop, mtu in enumerate(mtu_path): max_data = max_fragment_data(mtu) new_fragments = [] for frag in current_fragments: if frag <= max_data: # No fragmentation needed new_fragments.append(frag) else: # Fragment this piece sub_frags = fragment_single_hop(frag, mtu) new_fragments.extend(sub_frags) print(f"After Hop {hop+1} (MTU={mtu}): {len(new_fragments)} fragments") current_fragments = new_fragments return current_fragments # Scenario: 8000-byte datagram through decreasing MTUsprint("Multi-Hop Fragmentation Analysis")print("="*50)print("Original datagram: 8000 bytes data\n") mtu_path = [1500, 1000, 576]final_fragments = cascade_fragmentation(8000, mtu_path) print(f"\nFinal Result: {len(final_fragments)} fragments")print(f"Fragment sizes: {final_fragments[:10]}{'...' if len(final_fragments) > 10 else ''}")print(f"Total data: {sum(final_fragments)} bytes") # Compare to single-hop at minimum MTUprint(f"\nComparison: Direct fragmentation at MTU 576 would produce:")single_hop_count = math.ceil(8000 / max_fragment_data(576))print(f" {single_hop_count} fragments (same as cascaded result)")Important Observation:
The final fragment count depends only on the minimum MTU along the path, not on the order or number of fragmentation points. Whether fragmentation happens all at once or in stages, the end result is the same number of fragments.
Why?
Each intermediate fragmentation produces fragments sized for that hop's MTU. When those encounter a smaller MTU, they're further divided. The process continues until all fragments fit through the minimum MTU. The final state is equivalent to fragmenting once at the minimum MTU.
Practical Formula for Multi-Hop:
Final Fragment Count = ⌈ Original_Data / max_fragment_data(Min_MTU) ⌉
This is why Path MTU Discovery is valuable—knowing the minimum MTU in advance allows the source to create optimally-sized fragments once, avoiding the processing overhead of cascading fragmentation at each hop.
Fragment count directly impacts datagram delivery reliability. Since ALL fragments must arrive for successful reassembly, more fragments means higher failure probability.
The All-or-Nothing Problem:
If any single fragment is lost:
Probability Analysis:
Assume each packet has independent probability p of successful delivery.
For a datagram fragmented into N fragments:
P(datagram success) = p^N
This exponential relationship is why fragmentation is problematic:
| Fragments (N) | p=99% | p=98% | p=95% | p=90% |
|---|---|---|---|---|
| 1 | 99.0% | 98.0% | 95.0% | 90.0% |
| 2 | 98.0% | 96.0% | 90.3% | 81.0% |
| 3 | 97.0% | 94.1% | 85.7% | 72.9% |
| 5 | 95.1% | 90.4% | 77.4% | 59.0% |
| 7 | 93.2% | 86.8% | 69.8% | 47.8% |
| 10 | 90.4% | 81.7% | 59.9% | 34.9% |
| 20 | 81.8% | 66.8% | 35.8% | 12.2% |
| 45 | 63.6% | 40.1% | 9.9% | 0.9% |
12345678910111213141516171819202122232425262728293031323334353637383940
def datagram_success_probability(fragments, packet_success_rate): """Calculate probability of complete datagram delivery.""" return packet_success_rate ** fragments def expected_retransmissions(fragments, packet_success_rate): """ Expected number of datagram transmission attempts until success. Geometric distribution: E[attempts] = 1/p where p = datagram success rate """ datagram_p = datagram_success_probability(fragments, packet_success_rate) if datagram_p == 0: return float('inf') return 1 / datagram_p def wasted_bandwidth_factor(fragments, packet_success_rate): """ Factor by which bandwidth usage increases due to retransmissions. Ideally we send 'fragments' packets once. With retransmissions, we send fragments × E[attempts]. """ attempts = expected_retransmissions(fragments, packet_success_rate) return attempts # Analysis for a network with 98% packet delivery rateprint("Fragmentation Impact Analysis (98% packet success rate)")print("="*60) for frags in [1, 3, 5, 7, 10, 20]: datagram_p = datagram_success_probability(frags, 0.98) retrans = expected_retransmissions(frags, 0.98) waste_factor = wasted_bandwidth_factor(frags, 0.98) print(f"Fragments: {frags:2d} | Datagram Success: {datagram_p*100:5.1f}% | " f"Avg Attempts: {retrans:.2f} | Bandwidth: {waste_factor:.2f}x") print("\n" + "="*60)print("Key Insight: Even small increases in fragments significantly")print("degrade reliability and efficiency on lossy links.")On a typical Internet path with ~1-2% packet loss, a 10-fragment datagram has only ~82-90% success probability per attempt. This is why modern protocols avoid fragmentation: TCP uses MSS negotiation, and applications use DF flag with PMTUD to size datagrams appropriately.
Different protocols encapsulate data differently, affecting IP datagram sizes and fragment counts. Understanding these encapsulations enables accurate fragment predictions.
Common Protocol Encapsulation Overheads:
| Protocol | Header Size | Notes |
|---|---|---|
| IP | 20-60 bytes | Minimum 20, up to 60 with options |
| TCP | 20-60 bytes | Minimum 20, up to 60 with options |
| UDP | 8 bytes | Fixed size |
| ICMP | 8+ bytes | Varies by message type |
| GRE | 4-16 bytes | Depends on options enabled |
| IPsec ESP | 10-22+ bytes | Plus auth data and padding |
| VXLAN | 50 bytes | Outer Ethernet + IP + UDP + VXLAN |
Example: UDP Application Data Fragmentation
Application sends 8,192 bytes of UDP data:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
import math def calculate_udp_fragments(app_data_size, mtu=1500): """ Calculate fragments for UDP application data. Args: app_data_size: Application-level data (UDP payload) mtu: Network MTU Returns: dict with calculation details """ UDP_HEADER = 8 IP_HEADER = 20 # Total IP datagram size ip_total = app_data_size + UDP_HEADER + IP_HEADER # IP payload (data to be fragmented) ip_payload = app_data_size + UDP_HEADER # Max fragment data max_frag_data = ((mtu - IP_HEADER) // 8) * 8 # Fragment count num_fragments = math.ceil(ip_payload / max_frag_data) return { 'app_data': app_data_size, 'ip_datagram': ip_total, 'ip_payload': ip_payload, 'max_frag_data': max_frag_data, 'fragments': num_fragments, 'first_frag_app_data': max_frag_data - UDP_HEADER, # First frag carries UDP header 'last_frag_data': ip_payload - (num_fragments - 1) * max_frag_data } # Example calculationsprint("UDP Fragmentation Analysis")print("="*60) for app_size in [1000, 1472, 1473, 8192, 65507]: result = calculate_udp_fragments(app_size) print(f"\nApp data: {app_size} bytes") print(f" IP datagram: {result['ip_datagram']} bytes") print(f" IP payload (to fragment): {result['ip_payload']} bytes") print(f" Fragments: {result['fragments']}") if result['fragments'] > 1: print(f" First fragment: {result['max_frag_data']} bytes IP data " f"({result['first_frag_app_data']} bytes app data)") print(f" Last fragment: {result['last_frag_data']} bytes") print("\n" + "="*60)print("Note: 1472 bytes is max UDP payload without fragmentation on Ethernet.")print(" 1472 + 8 (UDP) + 20 (IP) = 1500 bytes = Ethernet MTU")On Ethernet (MTU 1500): Max UDP payload = 1500 - 20 (IP) - 8 (UDP) = 1472 bytes. Sending 1473+ bytes of UDP data guarantees fragmentation. DNS uses 512 bytes by default specifically to avoid fragmentation across all link types.
Let's solve comprehensive problems that integrate fragment count calculations with real-world scenarios.
Problem 1: VPN Tunnel Fragmentation
A site-to-site VPN uses IPsec ESP tunnel mode. Calculate fragments for a 1400-byte TCP data transfer.
Given:
123456789101112131415161718192021222324252627282930313233343536373839404142434445
import math # Problem 1: VPN Tunnel Fragmentationprint("Problem 1: IPsec VPN Tunnel")print("="*50) # Given valuesapp_data = 1400tcp_header = 20inner_ip_header = 20esp_overhead = 22 # 8 + 2 + 12outer_ip_header = 20mtu = 1500 # Calculate total encapsulated sizeinner_packet = app_data + tcp_header + inner_ip_header # 1440encapsulated = inner_packet + esp_overhead # 1462 (ESP encrypts inner packet)outer_datagram = encapsulated + outer_ip_header # 1482 print(f"Inner packet (TCP+IP): {inner_packet} bytes")print(f"After ESP: {encapsulated} bytes")print(f"Outer IP datagram: {outer_datagram} bytes") # Check if fragmentation neededif outer_datagram <= mtu: print(f"Result: NO fragmentation (fits in MTU {mtu})")else: outer_data = encapsulated max_frag_data = ((mtu - outer_ip_header) // 8) * 8 fragments = math.ceil(outer_data / max_frag_data) print(f"Result: {fragments} fragment(s) needed") print("\n" + "="*50)print("Problem 2: What is the maximum app data without fragmentation?")print("="*50) # Work backwards from MTUmax_outer_data = mtu - outer_ip_header # 1480 bytes for ESP payloadmax_inner_packet = max_outer_data - esp_overhead # 1458 bytesmax_app_data = max_inner_packet - tcp_header - inner_ip_header # 1418 bytes print(f"Max ESP payload: {max_outer_data} bytes")print(f"Max inner packet: {max_inner_packet} bytes")print(f"Max application data: {max_app_data} bytes")print("\nApplications should limit data to 1418 bytes to avoid fragmentation.")Problem 2: Variable MTU Path
Calculate the total fragments for a 6,000-byte datagram traversing:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
import math def max_frag_data(mtu, header=20): return ((mtu - header) // 8) * 8 print("Problem 2: Variable MTU Path")print("="*50) original_data = 6000mtu_path = [1500, 1200, 800, 576] print(f"Original datagram: {original_data} bytes data")print(f"Path MTUs: {mtu_path}")print() # Method 1: Calculate fragments at minimum MTUmin_mtu = min(mtu_path)final_max_data = max_frag_data(min_mtu)final_fragments = math.ceil(original_data / final_max_data) print("Method 1: Use minimum MTU")print(f" Minimum MTU: {min_mtu}")print(f" Max fragment data: {final_max_data} bytes")print(f" Final fragments: {final_fragments}") # Method 2: Simulate hop-by-hop fragmentationprint("\nMethod 2: Simulate cascade (verification)") current_fragments = [original_data] # Start with original for hop, mtu in enumerate(mtu_path): new_fragments = [] max_data = max_frag_data(mtu) for frag in current_fragments: if frag <= max_data: new_fragments.append(frag) else: # Fragment this piece remaining = frag while remaining > 0: piece = min(remaining, max_data) new_fragments.append(piece) remaining -= piece print(f" After Link {hop+1} (MTU={mtu:4d}): {len(new_fragments):2d} fragments") current_fragments = new_fragments print(f"\nFinal count: {len(current_fragments)} fragments")print(f"Same as Method 1: {len(current_fragments) == final_fragments} ✓")For any multi-hop fragmentation problem, the final fragment count equals ⌈data ÷ max_frag_data(min_MTU)⌉. The path order and intermediate MTUs don't affect the final count—only the minimum MTU matters.
We've covered all aspects of calculating the number of fragments. Let's consolidate the essential formulas and insights.
| Link Type | MTU | Max Frag Data | 10KB Data Fragments |
|---|---|---|---|
| Ethernet | 1500 | 1480 | 7 |
| PPPoE | 1492 | 1472 | 7 |
| GRE Tunnel | 1476 | 1456 | 7 |
| IPv6 in IPv4 | 1480 | 1460 | 7 |
| IPsec Tunnel | ~1400 | ~1376 | 8 |
| Token Ring | 4464 | 4440 | 3 |
| Minimum IP | 576 | 552 | 19 |
What's Next:
Now that we can calculate fragment sizes, offsets, and counts, the next page examines header overhead—the efficiency cost of fragmentation. We'll quantify exactly how much bandwidth is consumed by duplicate headers and explore optimization strategies.
You can now calculate the number of fragments for any scenario—simple or complex, single-hop or multi-hop. You also understand the reliability implications that make fragmentation avoidance a network design priority.