WireGuard MTU is the path MTU minus 80 bytes, the worst case for an IPv6 outer header (minus 60 if the endpoint is only ever reached over IPv4). A standard 1500 byte Ethernet path gives 1420, PPPoE at 1492 gives 1412, and a path you cannot measure gets 1280. TCP MSS is that MTU minus 40 for IPv4.
The number matters because of one specific failure. The tunnel comes up. wg show prints a recent handshake. Ping works, SSH works, DNS works. Then a web page loads halfway and hangs, apt update freezes at 0%, and a file copy stalls at a few kilobytes. Nothing in the logs. That pattern is almost never a key problem or a firewall problem. It is MTU, and the arithmetic behind it is small enough to do on paper.
If you want the number without the reading, the mesh topology designer on this site computes a WireGuard MTU for the common transports alongside the peer configuration it generates.
WireGuard overhead: where the 60 and 80 bytes come from
WireGuard data packets carry a transport header and authentication tag inside UDP and an outer IP packet. The fixed header overhead is straightforward to calculate.
| Layer | Bytes | Notes |
|---|---|---|
| WireGuard transport header | 16 | message type, reserved bytes, receiver index, 64-bit counter |
| Poly1305 authentication tag | 16 | appended to every encrypted payload |
| UDP header | 8 | source port, destination port, length, checksum |
| Outer IPv4 header | 20 | no options in normal use |
| Outer IPv6 header | 40 | fixed length, no options in normal use |
Fixed encapsulation overhead: 60 bytes over IPv4, 80 bytes over IPv6, assuming an IPv4 header without options or an IPv6 header without extension headers. These totals exclude any extra tunnel underneath WireGuard. The protocol also pads data toward a multiple of 16 bytes; the fixed header totals do not mean every small packet grows by exactly 60 or 80 bytes.
The Linux module does the same sum in device.c: the 32 byte minimum data message, the UDP header and the larger IP header, subtracted from 1500 for a default of 1420.
If you are asking whether OpenVPN’s DCO module changes the MTU math, it does not: DCO offloads OpenVPN’s data channel processing and leaves WireGuard’s encapsulation untouched.
The calculator
Take the MTU of the path the encrypted UDP actually travels over, subtract 80, and use that as the WireGuard interface MTU. Subtracting 80 rather than 60 costs 20 bytes of payload and buys immunity to the day your ISP turns on IPv6 and the outer header grows.
| Underlying path | Path MTU | WireGuard MTU | IPv4 TCP MSS | IPv4-only endpoint |
|---|---|---|---|---|
| Ethernet or fibre, standard | 1500 | 1420 | 1380 | 1440 |
| PPPoE (DSL, some fibre) | 1492 | 1412 | 1372 | 1432 |
| 6rd, 6in4, generic IP-in-IP | 1480 | 1400 | 1360 | 1420 |
| DS-Lite (IPv4 in IPv6), some cloud overlays | 1460 | 1380 | 1340 | 1400 |
| Constrained mobile path | 1430 | 1350 | 1310 | 1370 |
| Conservative universal floor | 1360 | 1280 | 1240 | 1300 |
| WireGuard inside another WireGuard tunnel | 1420 | 1340 | 1300 | 1360 |
TCP MSS is the WireGuard MTU minus 40 for IPv4 inside the tunnel (20 bytes of IP header plus 20 of TCP header), or minus 60 for IPv6 inside. The last column subtracts 60 and holds only while both endpoints reach each other over IPv4.
For any other path MTU, this shell function does the same arithmetic:
wgmtu() { # usage: wgmtu <path-mtu> [overhead, default 80; 60 if IPv4-only]
local mtu=$(( $1 - ${2:-80} ))
echo "WireGuard MTU $mtu, TCP MSS $((mtu - 40)) IPv4 / $((mtu - 60)) IPv6"
}
wgmtu 1452 # WireGuard MTU 1372, TCP MSS 1332 IPv4 / 1312 IPv6
Two limits are worth remembering. RFC 8200 requires every link carrying IPv6 to support a 1280 byte MTU, so if IPv6 runs inside the tunnel, do not configure the WireGuard interface below 1280. And a tunnel MTU of 1280 is the value to reach for when a path refuses to be measured: it is almost always safe, and the cost is roughly a tenth of your per-packet payload against 1420, not a multiple.
Why wg-quick usually lands on 1420
wg-quick does not hardcode a value. When MTU is absent from the [Interface] block, it looks up the route to the peer endpoint, falls back to the system default route if there is no endpoint to look up, and subtracts the worst case 80 bytes from whatever MTU it finds. On an ordinary 1500 byte Ethernet path that produces 1420, which is where the familiar number comes from. On a 1492 byte PPPoE line it produces 1412 automatically.
That automatic discovery fails most often in one situation, and it is a common one: when the local link claims 1500 but something between you and the peer carries less. The kernel has no idea a mid-path device is smaller, so it hands wg-quick 1500, wg-quick hands you 1420, and every packet near full size disappears silently.
The script also calculates only once, at up, so a laptop that moves to a phone hotspot keeps the old value, and any MTU = line, even a tutorial’s MTU = 1420, switches detection off.
Does WireGuard fragment packets?
No. WireGuard encrypts each inner packet into exactly one UDP datagram and never splits one itself. Fragmentation happens on either side of it: the kernel fragments or refuses inner packets larger than the WireGuard interface MTU before encryption, and the outer IP layer fragments encrypted datagrams larger than the path. The outer kind is what stalls tunnels.
The Linux module sets ignore_df and passes 0 as the DF value to udp_tunnel_xmit_skb, so an oversized outer IPv4 datagram is fragmented rather than dropped. IPv6 routers cannot: under RFC 8200 only source nodes fragment, so an oversized outer IPv6 packet dies mid-path unless an ICMPv6 Packet Too Big message gets back.
RFC 8900 explains why fragmented outer packets are fragile: only the first fragment carries the UDP ports, NATs must virtually reassemble the rest, and some middleboxes discard fragments outright. Lose one fragment and the whole WireGuard packet is gone, which is why one config can work on one network and stall on another.
To see whether outer packets are being split, capture on the physical interface:
sudo tcpdump -ni eth0 'host 203.0.113.10 and ip[6:2] & 0x1fff != 0'
Per pcap-filter(7), ip[6:2] & 0x1fff = 0 matches unfragmented datagrams and first fragments, so != 0 catches the trailing pieces a udp port 51820 filter would miss. Any output during a large transfer means the WireGuard MTU is too high.
Measure the path instead of guessing
Do this from the machine that terminates the tunnel, against the peer’s public endpoint address, with the tunnel down or at least with the traffic routed outside it. The do-not-fragment flag is what makes the test meaningful.
ping -M do -s 1472 vpn.example.net
ICMP adds 28 bytes on IPv4, 8 for the ICMP header and 20 for the IP header, so a payload of 1472 is a 1500 byte packet on the wire. If it replies, your path is a clean 1500. If it returns Message too long or simply times out while smaller pings succeed, step down and repeat:
for s in 1472 1452 1432 1412 1392 1372 1352 1332; do
ping -M do -c1 -W2 -s "$s" vpn.example.net >/dev/null 2>&1 \
&& { echo "largest payload that passes: $s -> path MTU $((s+28))"; break; }
done
Add 28 to the largest payload that passes to get the path MTU, then subtract 80 for the WireGuard MTU. A path MTU of 1452 gives a payload of 1424 and a WireGuard MTU of 1372.
Once the tunnel is up with that value, confirm from inside it. The target is the far end’s tunnel address, and the payload to test is the interface MTU minus 28:
ping -M do -c3 -s 1392 10.7.0.1
A 1420 byte interface should pass 1392 and fail 1393. If it fails well below that, the interface MTU is still too high for the path.
Set it on both ends
MTU is a property of an interface, not of a session. Each end fragments or refuses to fragment based on its own configured value, so a low path affects traffic in both directions and both ends need the lower number.
[Interface]
PrivateKey = <private key>
Address = 10.7.0.1/24
ListenPort = 51820
MTU = 1412
Change it live without editing files by setting it on the link, which is useful while bisecting:
sudo ip link set mtu 1412 dev wg0
That reverts on the next wg-quick down and up cycle, so write the working value into the config once you have it.
WireGuard MTU settings by platform
The wg-quick, Windows, macOS, iOS and Android clients all read an MTU = line in [Interface], so setting it is the same everywhere. What differs is the value each picks when the line is missing.
| Where WireGuard runs | How to set the MTU | Default when unset |
|---|---|---|
| wg-quick on Linux, including a Raspberry Pi | MTU = 1412 in [Interface] | Route MTU minus 80, computed once at up |
| systemd-networkd | MTUBytes=1412 under [NetDev] in the .netdev file | 1420, the kernel module’s fixed default, not route-aware like wg-quick |
| NetworkManager | nmcli connection modify wg0 wireguard.mtu 1412 | A default that ignores current routes |
| Windows app | MTU = 1412, accepted range 576 to 65535 | Default route MTU minus 80, tracked live |
| macOS app | MTU = 1412 | Derived by macOS from 80 bytes of overhead |
| iOS app, Android app (userspace backend) | MTU = 1412 | 1280 |
| MikroTik RouterOS | /interface wireguard set wg0 mtu=1412 | 1420 |
Windows re-runs its calculation whenever the default route changes, with a floor of 576 for IPv4 and 1280 for IPv6, but only when no MTU is configured. The mobile 1280 is deliberate: the Apple source says “in practice there are too many broken networks out there,” and the Android Go backend falls back to 1280. If a config works on a phone but stalls on a laptop, compare their MTUs first.
Clamping TCP MSS, and why it is only half a fix
On a router that terminates the tunnel for a whole LAN you cannot reach into every client to set an MTU. The standard answer is to rewrite the MSS option in TCP SYN packets crossing the tunnel so both endpoints agree to send smaller segments.
With iptables:
sudo iptables -t mangle -A FORWARD -o wg0 -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --clamp-mss-to-pmtu
With nftables:
table inet mangle {
chain forward {
type filter hook forward priority mangle;
oifname "wg0" tcp flags syn tcp option maxseg size set rt mtu
}
}
Both are worth having. Neither is complete, for a specific reason: MSS clamping only affects TCP. QUIC, which now carries a large share of web traffic through HTTP/3, runs over UDP and never sees an MSS option. Neither do DNS over UDP, most VPN-inside-VPN traffic, or media streams over UDP. If the interface MTU is wrong, clamping hides the problem for HTTPS over TCP while UDP applications keep stalling in ways that look unrelated.
The order that works: set a correct interface MTU first, then clamp MSS as a safety net for clients you do not control.
Why path MTU discovery does not rescue you
Classic path MTU discovery depends on routers returning ICMP “fragmentation needed” (IPv4) or “packet too big” (IPv6) messages. Large numbers of networks filter inbound ICMP wholesale, so the sender never learns the path is smaller and simply keeps retransmitting packets that are silently dropped. RFC 4821 documents this failure mode as an ICMP black hole and specifies packetization layer path MTU discovery, which probes with real payload data instead of relying on ICMP, as the fix. Linux implements it and the behaviour is controlled by net.ipv4.tcp_mtu_probing:
sudo sysctl -w net.ipv4.tcp_mtu_probing=1
A value of 1 enables probing only after a suspected black hole is detected, which is the safe setting. Treat it as a mitigation for paths you do not control, not a substitute for a correct MTU on your own interface.
Two related traps are worth naming. If your own firewall drops all ICMP inbound, you have created the black hole yourself, so permit ICMP type 3 code 4 on IPv4 and ICMPv6 type 2 at minimum. And if containers run behind the tunnel, the container bridge MTU must be no larger than the tunnel MTU; a Docker bridge sitting at 1500 behind a 1412 tunnel reproduces the exact stall this article is about, one layer further in.
A diagnostic order for the stall
wg showreports a recent handshake, so the crypto and the endpoint are fine.- Small pings pass through the tunnel, so routing and
AllowedIPsare fine. If they do not, the problem is upstream of MTU; start with the fundamentals of keys, AllowedIPs, and NAT traversal. ping -M do -s 1392inside the tunnel fails while-s 500passes. That is the confirmation.- Lower the interface MTU on both ends until the large ping passes, or measure the underlay first with the loop above and compute the value directly.
- Clamp MSS on any router that forwards for clients you cannot configure.
- Re-test with something UDP based, not only a browser, so a clamp does not mask a still-wrong MTU.
When the MTU is right and it still stalls
A handful of causes imitate an MTU problem closely enough to waste an afternoon. Offload features are the usual suspect: generic receive offload and segmentation offload on the physical NIC can hand oversized segments to a tunnel that cannot carry them, and disabling them with ethtool -K eth0 gro off gso off tso off is a quick way to rule it in or out. A second tunnel in the path, such as WireGuard inside a corporate VPN or over a mobile hotspot that itself tunnels, means subtracting overhead twice. And a NAT mapping expiring mid-transfer produces a stall that looks identical for the first few seconds, which is why PersistentKeepalive = 25 belongs on the NAT’d side before you start bisecting MTU values.
If you are still building the tunnel rather than repairing one, the step by step Ubuntu server and client setup walks through the configuration with a sane MTU already in place, and the comparison of WireGuard against Tailscale covers when an overlay’s NAT traversal and relays are worth its control plane.
Looking for a WireGuard AllowedIPs calculator?
That is a different subtraction, unrelated to MTU. WireGuard has no exclude syntax, so an AllowedIPs calculator removes the ranges you want kept out of the tunnel from 0.0.0.0/0 and lists the prefixes that remain. Python’s address_exclude does it in one line:
python3 -c 'import ipaddress as ip; print(", ".join(map(str, sorted(ip.ip_network("0.0.0.0/0").address_exclude(ip.ip_network("192.168.1.0/24"))))))'
Excluding one /24 leaves 24 prefixes; what to set in AllowedIPs on each peer covers when a more specific route is the simpler tool.
See also
FAQ
what is the default mtu for wireguard?
On Linux it is 1420: the kernel module sets 1500 minus 80 when an interface is created, and wg-quick lands on the same figure over a 1500 byte route. MikroTik RouterOS also defaults to 1420. The iOS app and the Android app’s userspace backend use 1280, and Windows subtracts 80 from the default route’s MTU.
does wireguard mtu have to match on both sides?
No. The handshake carries no MTU field, so a tunnel with 1420 on one end and 1280 on the other comes up normally. Each end sizes only the packets it sends, so if the path is smaller, the end with the higher value is the one whose large packets vanish. Set both ends to the lower, measured number.
can wireguard mtu be higher than 1420?
Yes, when the path between the endpoints carries more than 1500 bytes, as a LAN with jumbo frames can; subtract 80 from that MTU as usual. Over the internet, anything above 1420, or 1440 for an IPv4-only endpoint, makes full-size outer UDP packets too big for a standard 1500 byte path, so they are fragmented or dropped.