What Is MTU and MSS Clamping in Proxy Connections
Small requests work, large ones hang forever? Learn MTU vs MSS, PMTUD black holes, and how MSS clamping fixes stalled proxy and tunnel connections.

Short answer: MTU is the largest packet a link will carry, MSS is the largest TCP payload that fits inside it, and MSS clamping is a rewrite of the TCP MSS option in transiting SYN packets that forces both ends of a proxy connection to use segments small enough for the real path, which cures the classic "small requests work, large ones hang forever" failure.
This is the bug that wastes an entire afternoon. Your proxy authenticates. curl prints response headers. A 2 KB JSON endpoint returns instantly. Then you request a real page, the transfer freezes at a few kilobytes, and the socket sits there until your client's timeout fires. Nothing in the proxy logs looks wrong, because from the proxy's point of view nothing is wrong: it sent the bytes. They just never made it across one specific hop.
Key takeaways
- MTU is an IP-layer limit on total packet size. MSS is a TCP-layer limit on payload size, advertised once per direction in the SYN and never renegotiated afterwards.
- Path MTU Discovery (RFC 1191) only works if ICMP "Fragmentation Needed" messages get back to the sender. Firewalls that blanket-drop ICMP turn a path into a black hole.
- The signature is distinctive: handshakes and small responses succeed, the first full-size segment stalls, and the stall is reproducible at the same byte offset.
- With a forward proxy there are two TCP connections and two path MTUs. Lowering your laptop's MTU does nothing if the small hop is on the proxy's egress path.
- Clamp the MSS on the router that owns the low-MTU link, in both SYN directions, or lower the tunnel interface MTU and let the kernel derive the MSS.
The Symptom Nobody Attributes to MTU
An MTU problem does not look like a network problem. It looks like a flaky target site, a bad proxy exit, or an anti-bot system stalling you on purpose. Here is what actually happens, in order.
The TCP handshake is three tiny packets, well under 100 bytes each. It completes. The TLS ClientHello is typically 300 to 700 bytes, so it goes out fine. The server's flight, meaning ServerHello plus the certificate chain, is often 3 to 5 KB, which is why a badly broken path hangs during the handshake while a mildly broken one squeaks through. Your HTTP request and the response headers are small. Then the origin starts pushing the body at full segment size, the first oversized packet meets a hop that cannot carry it, and everything stops.
What you observe:
| What you see | What it looks like | What it actually is |
|---|---|---|
| `curl -I` works, `curl` on the same URL hangs | Target blocking bots | First full-size body segment dropped |
| Small API responses fine, large pages time out | Rate limiting | Size-dependent packet loss |
| Stall always at roughly the same byte count | Server-side throttle | Same hop, same limit, every time |
| Works on your laptop, fails from the scraper VM | Datacenter IP reputation | VM sits behind a tunnel with lower MTU |
| Retries never help, and never fail fast either | Slow origin | TCP retransmitting a packet that can never fit |
Three properties separate this from every other failure. It's deterministic: the same URL stalls at the same place every time. It's size-dependent, not host-dependent: https://httpbin.org/bytes/1024 returns and https://httpbin.org/bytes/65536 does not, through the same proxy, in the same second. And it's silent: no RST, no 502, no error string, just a socket that eventually hits your read timeout. With no error code to search for, most people never open a packet capture, which is why this bug survives so long. Our breakdown of proxy error codes covers the failures that do announce themselves. This one doesn't.
MTU vs MSS: Two Different Limits
They get used interchangeably in forum posts. They are not the same thing, they live at different layers, and the difference is the entire fix.
MTU (Maximum Transmission Unit) is a property of a link. It's the largest IP packet, headers included, that an interface will put on the wire. Standard Ethernet is 1500 bytes. It's a local fact about one hop, not something either endpoint negotiates.
MSS (Maximum Segment Size) is a property of a TCP connection. It's the largest TCP payload, headers excluded, that one side is willing to receive. It travels as TCP option kind 2, length 4, a 16-bit value, and it appears only in SYN segments, as specified in RFC 9293.
The arithmetic:
| Path | MTU | IP header | TCP header | MSS |
|---|---|---|---|---|
| Ethernet, IPv4 | 1500 | 20 | 20 | 1460 |
| Ethernet, IPv6 | 1500 | 40 | 20 | 1440 |
| PPPoE (DSL), IPv4 | 1492 | 20 | 20 | 1452 |
| IPv6 minimum link | 1280 | 40 | 20 | 1220 |
| No MSS option present, IPv4 | assume 576 | 20 | 20 | 536 |
Two consequences trip people up constantly.
MSS is per-direction and it is not a negotiation
Each side advertises what it can receive. There is no agreement on a common value. If your client says 1460 and the proxy says 1380, the proxy sends you segments of at most 1460 and you send it segments of at most 1380. This is why clamping has to touch both the SYN and the SYN-ACK. Fixing one direction fixes only that direction, and since the payload usually flows toward the client, that's often the direction people forget.
The number on the wire is smaller than the MSS
With TCP timestamps enabled, which is the default on Linux, the options field eats 12 bytes out of every segment. A connection with MSS 1460 therefore emits data segments of 1448 bytes. If you're reading a capture and wondering why nothing is exactly 1460, that's why. It also means your effective headroom is 12 bytes tighter than the MSS math suggests.
Once the SYN exchange is over, the advertised MSS is frozen for the life of the connection. TCP can still lower how much it actually sends per segment if it learns the path is smaller. That learning step is Path MTU Discovery, and it's the part that breaks.
Scraping at scale? Skip the blocks.
Fast, unblockable datacentre proxies with unlimited bandwidth.
Path MTU Discovery Depends on One ICMP Message
RFC 1191, published in November 1990, defines how a sender finds the smallest MTU along a path without any configuration. The mechanism is elegant and it is also a single point of failure.
- The sender sets the Don't Fragment (DF) bit on outgoing IPv4 packets and starts at the local link MTU.
- A router whose next hop has a smaller MTU cannot fragment a DF packet, so it drops it.
- That router returns ICMP Type 3 (Destination Unreachable), Code 4 (Fragmentation Needed and DF Set). RFC 1191 put the next-hop MTU into a previously unused 16-bit field of that ICMP header, so the message states exactly how small to go.
- The sender caches that MTU for the destination and retransmits with smaller segments.
For IPv6 the equivalent is ICMPv6 Type 2, Packet Too Big, specified in RFC 8201.
The whole design assumes one thing: the ICMP error reaches the original sender and can be matched to the socket that caused it. Both halves of that assumption fail regularly.
PMTUD Black Holes
A PMTUD black hole is a path where oversized packets are dropped and the ICMP error never arrives. The sender has no signal at all, so it does the only thing TCP knows how to do: retransmit the same too-large segment, back off exponentially, retransmit again, and eventually give up. RFC 2923 catalogued these failure modes back in 2000 and they are still everywhere in 2026.
The usual causes:
- Blanket ICMP filtering. Someone applied
deny icmp any anybecause "ICMP is a security risk," which kills Type 3 Code 4 along with ping. - Cloud security groups and NACLs that only permit the ports an application needs. AWS security groups need an explicit ICMP rule: allowing TCP 443 does not allow the ICMP that TCP 443 depends on.
- NAT devices that mangle the embedded header. The ICMP error quotes the front of the original packet, and a NAT box has to translate the addresses inside that quoted header too. Cheap ones don't, so the error arrives, fails to match any socket, and gets discarded. That looks identical to a black hole in every tool except a capture.
- Anycast and ECMP asymmetry, where the ICMP returns along a path that discards it.
- The sender is not you. With a proxy in the middle, the ICMP goes to whichever machine emitted the oversized packet. That's the proxy or the origin server, not your client.
The last one matters most at scale, and it deserves its own section.
Why Proxy Connections Hit This More Often
A forward proxy does not forward packets. It terminates your TCP connection and opens a separate one to the target. That holds for an HTTP proxy, for a CONNECT tunnel, and for SOCKS5. The consequences for MTU are direct and mostly unappreciated:
- There are two MSS negotiations, not one. Your client and the proxy settle on one value. The proxy and the origin settle on a completely different one. Neither knows about the other.
- There are two path MTUs. Your
pingandtracepathresults describe the client-to-proxy leg only. They say nothing about proxy-to-origin. - PMTUD state lives on the proxy. If the black hole is on the egress side, the proxy's kernel is the machine that needs the ICMP and the machine that caches the result. You cannot see or fix that from your laptop.
- Lowering your local MTU changes nothing when the broken hop is downstream of the proxy. This is the single most common wasted hour in MTU debugging.
Then there's the exit network. Residential and mobile exits are the worst offenders, because the last hop is frequently a PPPoE line at 1492, a carrier tunnel somewhere in the 1400s, or a CGNAT gateway doing its own encapsulation. Datacenter exits are usually a clean 1500 unless someone put them behind a VPN. If you chain proxies, every hop adds another candidate for the smallest link, and the chain's effective MTU is the minimum across all of them.
One more asymmetry: because a proxy terminates TCP, its egress interface MTU determines the MSS it advertises to the origin. On a proxy host you generally don't clamp at all. Set the interface MTU correctly and the kernel does the right thing. Clamping is for boxes that route traffic, not boxes that terminate it.
Tunnel and Encapsulation Overhead
Every layer of encapsulation steals bytes from the payload. The outer headers count against the same 1500-byte budget, so the inner packet has to shrink.
| Encapsulation | Overhead over IPv4 | Effective MTU from 1500 | Safe IPv4 MSS |
|---|---|---|---|
| Plain Ethernet | 0 | 1500 | 1460 |
| PPPoE | 8 | 1492 | 1452 |
| GRE | 24 | 1476 | 1436 |
| VXLAN | 50 | 1450 | 1410 |
| WireGuard over IPv4 | 60 | 1440 | 1400 |
| WireGuard over IPv6 | 80 | 1420 | 1380 |
| IPsec ESP tunnel (AES-CBC + SHA1) | 52 to 58 | about 1442 | about 1402 |
| IPsec ESP + NAT-T (UDP 4500) | 60 to 66 | about 1434 | about 1394 |
This is why wg-quick defaults the WireGuard interface to 1420 rather than 1440: it budgets for an IPv6 outer header so the tunnel keeps working when the endpoint is reached over IPv6.
Overheads stack. A scraper in a container, on a cloud VM whose network runs at 1450, reaching a proxy gateway over WireGuard, is looking at 1450 minus 60, so 1390, before the container runtime gets a vote. Docker is a frequent culprit because docker0 defaults to 1500 no matter what the host interface is set to, so containers emit 1500-byte packets onto a 1450-byte path with nothing to warn them.
// /etc/docker/daemon.json - match the host's real MTU
{
"mtu": 1450
}
For a user-defined network, set it per network instead:
docker network create --opt com.docker.network.driver.mtu=1450 scrapers
IPv6 Makes the Failure Sharper
IPv6 removed in-path fragmentation entirely. Under RFC 8200, only the source node may fragment, and routers must never do it. There is no DF bit because the behaviour is always "don't fragment."
Three practical effects follow:
- PMTUD is mandatory, not an optimisation. In IPv4 you occasionally get rescued by a middlebox that clears DF and fragments the packet for you. That safety net does not exist in IPv6.
- Blocking ICMPv6 breaks IPv6. RFC 4890 spells out which ICMPv6 types must be permitted through a firewall, and Packet Too Big is at the top of the list. Treating ICMPv6 like ICMPv4 and dropping it is a straightforward outage.
- 1280 is the floor. Every IPv6 link must support at least 1280 bytes, which makes MSS 1220 the universally safe IPv6 clamp value.
The upside is that IPv6 fails cleanly. Either the path carries your size or it drops every packet at that size, so bisecting is fast. Test both stacks separately on dual-stack exits: it's routine to find a working IPv4 path and a black-holed IPv6 path to the same hostname, and since most clients prefer IPv6 under Happy Eyeballs, the broken stack is the one that gets used. Our IPv4 vs IPv6 proxies comparison covers the wider trade-offs.
A Concrete Diagnosis Recipe
Run these in order. The whole sequence takes about five minutes.
1. Confirm the stall is size-dependent
Before touching packets, prove the failure scales with response size. A ladder of increasing byte counts through the same proxy path settles it immediately:
for n in 512 1024 4096 16384 65536 262144; do
echo -n "$n: "
curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" --max-time 20 \
-x http://USER:PASS@proxy.sparkproxy.io:10000 \
"https://httpbin.org/bytes/$n"
done
If the small sizes return in milliseconds and everything above roughly 1.5 KB hits --max-time, stop looking at anti-bot systems. This is MTU.
The same ladder through the SparkProxy Scraping API isolates which leg is broken, because the API's own egress path replaces yours:
import requests
for n in (1024, 65536):
r = requests.get(
"https://scrape.sparkproxy.io/api/v1",
headers={"X-API-Key": "YOUR_API_KEY"},
params={"url": f"https://httpbin.org/bytes/{n}", "country_code": "US"},
timeout=60,
)
print(n, r.status_code, len(r.content))
If both sizes come back 200 through the API but the large one hangs over your own tunnel, the broken hop is on your side, not the exit's. A 510 here means the scrape itself failed and a 429 means you hit the concurrency limit. Neither is an MTU symptom.
2. Ping with DF set, walking the size down
The payload size you pass is not the packet size. For IPv4, add 28 bytes (20 IP + 8 ICMP) to get the total on the wire.
# Linux: -M do sets DF, -s is the ICMP payload
ping -M do -s 1472 -c 2 1.1.1.1 # = 1500-byte packet
ping -M do -s 1452 -c 2 1.1.1.1 # = 1480
ping -M do -s 1392 -c 2 1.1.1.1 # = 1420
ping -M do -s 1272 -c 2 1.1.1.1 # = 1300
# Windows: -f sets DF, -l is the payload
ping -f -l 1472 1.1.1.1
# macOS: -D sets DF
ping -D -s 1472 -c 2 1.1.1.1
Read the failure text carefully, because it distinguishes the two cases:
| Response | Meaning |
|---|---|
| `Frag needed and DF set (mtu = 1420)` | PMTUD works. A router told you the limit. |
| `ping: local error: message too long, mtu=1500` | Your own interface rejected it. Local MTU issue. |
| `Packet needs to be fragmented but DF set.` (Windows) | Same as row one, with the limit reported. |
| Nothing at all. 100% loss at this size, success one step down | **Black hole.** No ICMP is getting back. |
Silence is the diagnosis you're hunting for. Add 28 to the largest payload that still succeeds and you have the real path MTU.
3. Find the hop with tracepath
tracepath needs no root and reports the MTU per hop:
tracepath -n 1.1.1.1
1?: [LOCALHOST] pmtu 1500
1: 10.8.0.1 0.512ms
2: 10.8.0.1 0.470ms pmtu 1420
3: 198.51.100.9 12.331ms
Resume: pmtu 1420 hops 9 back 9
The pmtu 1420 annotation marks where the path narrows. Use tracepath6 for IPv6.
4. Read the MSS in the SYN
This is the definitive check, and almost nobody runs it. Capture the handshake and look at what each side advertised:
sudo tcpdump -ni any -v \
'tcp[tcpflags] & tcp-syn != 0 and port 443' -c 4
You want the options [mss NNNN,...] field on both the SYN and the SYN-ACK. Cleaner output with tshark:
sudo tshark -i any -Y 'tcp.flags.syn==1' \
-T fields -e ip.src -e ip.dst -e tcp.flags.ack -e tcp.options.mss_val
If your host advertises 1460 while the path is 1420, and no clamp is rewriting it, you've found the bug. If a clamp is already in place you'll see the rewritten value, which is how you confirm the fix landed.
5. Prove the ICMP is missing
Run this while triggering the stall. Zero output confirms the black hole:
sudo tcpdump -ni any 'icmp[icmptype] == 3 and icmp[icmpcode] == 4'
# IPv6:
sudo tcpdump -ni any 'icmp6 and ip6[40] == 2'
6. Check the kernel's cached PMTU
ss -ti state established dst 1.1.1.1 # look for mss: and pmtu:
ip route get 1.1.1.1 # a cached exception prints "mtu 1420"
MSS Clamping: The Fix and Where to Apply It
MSS clamping means intercepting SYN and SYN-ACK packets as they transit a router and rewriting the MSS option value downward, so both endpoints choose segments that fit the path from the very first byte. PMTUD never has to run, so its dependence on ICMP stops mattering.
The right place to clamp is the device that owns the small link: the VPN concentrator, the PPPoE router, the cloud NAT gateway, the container host.
Linux, iptables
# Clamp to the outgoing route's MTU (adaptive)
iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --clamp-mss-to-pmtu
# Or pin an explicit value when you know the real path ceiling
iptables -t mangle -A FORWARD -o wg0 -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --set-mss 1360
The --tcp-flags SYN,RST SYN match does quiet work here. It matches any packet with SYN set and RST clear, which covers the SYN and the SYN-ACK. That's what makes the clamp bidirectional. A rule matching only pure SYNs fixes half the connection and leaves you debugging the other half.
Linux, nftables
nft add rule inet filter forward tcp flags syn \
tcp option maxseg size set rt mtu
IPv6
Add the mirror rule. ip6tables uses the same extension:
ip6tables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --clamp-mss-to-pmtu
On tunnel software
WireGuard has no clamp option, so set the interface MTU and let the kernel derive the MSS:
ip link set dev wg0 mtu 1380
OpenVPN exposes mssfix in its config, which performs the same rewrite for traffic inside the tunnel.
Choosing a value
--clamp-mss-to-pmtu uses the MTU of the outgoing route on that box, which is correct for the local tunnel and blind to anything further along the path. If a hop three networks away is the narrow one, the adaptive clamp still overshoots and you need an explicit --set-mss. Common safe values: 1360 covers most VPN and cloud overlays, 1300 is very conservative and still efficient, 1220 is the IPv6 floor. Below roughly 1200 you start paying measurable throughput, since header overhead rises as a fraction of each segment.
What clamping cannot do
Clamping only touches TCP. UDP has no MSS option, so QUIC/HTTP3, DNS, and DTLS are untouched: a broken-MTU path can leave TLS over TCP working fine while large DNSSEC responses over UDP 53 still vanish. Clamping also can't help on a box that terminates connections rather than routing them, which is the proxy case above. Set the egress interface MTU there instead.
PLPMTUD: Discovery Without ICMP
The long-term answer to black holes is to stop depending on ICMP at all. RFC 4821, Packetization Layer Path MTU Discovery, published in March 2007, moves discovery inside the transport: send probe packets of increasing size and use the transport's own delivery confirmation to decide what fits. No ICMP required.
Linux implements it for TCP:
# 0 = off, 1 = enable only when a black hole is suspected, 2 = always on
sysctl -w net.ipv4.tcp_mtu_probing=1
sysctl -w net.ipv4.tcp_base_mss=1024
Setting tcp_mtu_probing=1 on a proxy or scraper host is cheap insurance. It stays dormant until retransmissions suggest a black hole, then probes downward instead of hanging forever. Because it reacts after the stall starts, treat it as a complement to a clamp rather than a replacement.
RFC 8899, published in September 2020, generalises the same idea to datagram transports, which is how QUIC handles path MTU. That produces a genuinely useful workaround: QUIC requires the path to carry at least 1200-byte UDP payloads and probes upward from there, so HTTP/3 frequently succeeds on paths where HTTP/2 black-holes. If a target supports HTTP/3 and your TCP connections stall on large bodies, curl --http3 is a fast way to confirm you're chasing an MTU problem and not a blocking problem.
Operational Checklist
Work down this list on any host that runs scrapers or proxy clients:
- Record the real path MTU to a few representative targets with the DF ping ladder. Don't assume 1500.
- Set tunnel interface MTUs explicitly. Never rely on the default when the underlay is a cloud network at 1450 or lower.
- Clamp MSS in the
FORWARDchain on every router that owns a low-MTU link, matchingSYN,RST SYNso both directions are covered. - Allow ICMP Type 3 Code 4 inbound, and ICMPv6 Type 2 for IPv6. This is not optional and it is not a meaningful attack surface.
- Match container MTU to host MTU in
daemon.jsonor per network. - Enable
net.ipv4.tcp_mtu_probing=1as a backstop. - Add a large-body request to your health checks. A proxy health check that only fetches a small JSON response reports green through a completely black-holed path.
- Test IPv4 and IPv6 separately on dual-stack exits.
Point seven is the cheapest fix on the list and the one most often skipped. Monitoring that only requests /ip or a status endpoint will never see this class of failure. Fetch at least 64 KB. And note that the protocol you use does not change the packet-size arithmetic: HTTP, HTTPS, and SOCKS5 all ride on TCP and all inherit the same path MTU.
Frequently asked questions
FAQ
MTU is the largest complete IP packet a network link will carry, including IP and TCP headers. MSS is the largest TCP payload one endpoint will accept, excluding those headers. For standard IPv4 Ethernet, MTU 1500 corresponds to MSS 1460, since the IP and TCP headers take 20 bytes each.
A PMTUD black hole is a network path that drops oversized packets without returning the ICMP "Fragmentation Needed" message that Path MTU Discovery relies on. The sender never learns to shrink its segments, so it retransmits the same too-large packet until the connection times out, producing a silent hang instead of an error.
Because handshakes, headers, and small responses all fit in packets below the path's real MTU, while the first full-size data segment does not. The connection establishes normally and then stalls the moment the origin starts sending at maximum segment size, which is the classic fingerprint of an MTU or MSS mismatch.
Use --clamp-mss-to-pmtu when the narrow link belongs to the router doing the clamping. When the narrow hop sits further downstream, pin an explicit value: 1360 covers most VPN and cloud overlay overhead, 1300 is conservative and still efficient, and 1220 is the safe floor for IPv6 since every IPv6 link must support 1280 bytes.
Slightly, and far less than the alternative. Clamping from 1460 to 1360 raises per-packet header overhead by roughly 7 percent of payload. Set against connections that hang for a full timeout and then get retried, that cost is negligible.
Yes, and the need is stronger. IPv6 routers never fragment in transit, so an oversized packet is always dropped rather than sometimes rescued. Clamp IPv6 traffic with the ip6tables TCPMSS target and make sure ICMPv6 Type 2 (Packet Too Big) is permitted through every firewall on the path.
Get 50% off your first month
Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.
Offer ends soon โ claim it before it's gone
Related articles

The HTTP CONNECT Method Explained
The HTTP CONNECT method at wire level: authority-form request lines, 200 Connection Established, 407 and 502 debugging, and why HTTPS resists inspection.

How Proxy Caching Works: Forward Proxy Cache Explained
How proxy caching works: Cache-Control and ETag revalidation, why an HTTPS CONNECT tunnel cannot be cached, and how a stale hit corrupts scraped data.

BGP and RIR IP Allocations: Why Proxy IP Origin Matters
An RIR IP allocation says who holds a proxy IP, BGP says where it is routed. Learn to audit both with RDAP, RPKI and geofeeds before you buy proxies.
