Skip to content
← ENGINEERING NOTES
NETWORKING7 min read

Why Your WireGuard Tunnel Is Slow

WireGuard slower than the link underneath it? Learn the real packet overhead, how to find the right MTU with ping, when to clamp MSS and how to check UDP drops.

BY ASHLIN DARIUS GOVINDASAMY

You paid for a gigabit line, you set up WireGuard, and your tunnel limps along at a fraction of it, or worse, SSH connects and then freezes the moment you cat a big file. Nine times out of ten the culprit is not WireGuard, it is packet size. This guide shows how to tune WireGuard on Linux: where the overhead comes from, how to measure the right MTU, when MSS clamping saves you, and how to check for UDP drops.

Where the bytes go

Every packet you send through WireGuard is wrapped in a new outer packet. The outer packet carries an IP header (20 bytes for IPv4, 40 for IPv6), a UDP header (8 bytes) and the WireGuard transport header and authentication tag (32 bytes: a 4-byte type, a 4-byte key index, an 8-byte nonce and a 16-byte tag). That is 60 bytes of overhead over IPv4 and 80 over IPv6. On a standard 1500-byte Ethernet link, 1500 minus 80 gives 1420, which is why that number is the familiar WireGuard default.

  • Outer IPv4: 20 + UDP 8 + WireGuard 32 = 60 bytes, so a 1500 link allows an inner MTU of 1440.
  • Outer IPv6: 40 + UDP 8 + WireGuard 32 = 80 bytes, so a 1500 link allows an inner MTU of 1420.
  • The outer protocol decides the budget, not the IP version of the traffic inside the tunnel.
  • If your underlay is not 1500 (PPPoE is 1492, many mobile and cloud paths are lower), subtract from the real path MTU instead.

Set the MTU too high and every large packet gets fragmented or dropped on the way out. Set it too low and you waste bandwidth on headers. Both look like 'the tunnel is slow'.

Measure the real path MTU

Do not guess, measure. The trick is to ping with the Don't Fragment bit set and a payload of known size. The ICMP payload plus 28 bytes (20 IP + 8 ICMP) is the packet size on the wire. Run this against your WireGuard endpoint's public address from the other peer, with the tunnel down or ignoring the tunnel route, to test the underlay.

bash
HOST=203.0.113.10   # the peer's public address

# 1472 payload + 28 header = 1500 byte packet
ping -c 3 -M do -s 1472 "$HOST"

# if it says "message too long" or loses packets, walk down
for size in 1472 1464 1452 1440 1400 1372 1280; do
  if ping -c 2 -W 2 -M do -s "$size" "$HOST" >/dev/null 2>&1; then
    echo "largest passing payload: $size  ->  path MTU $((size + 28))"
    break
  fi
done
Binary-search the largest packet that passes unfragmented

Take the path MTU you found and subtract the WireGuard overhead (80 to be safe on both IP versions, 60 if you are certain the outer transport is IPv4). Then set it explicitly on the interface.

ini
[Interface]
Address = 10.8.0.1/24
PrivateKey = <server private key>
ListenPort = 51820
MTU = 1420

[Peer]
PublicKey = <peer public key>
AllowedIPs = 10.8.0.2/32
/etc/wireguard/wg0.conf

If you leave MTU out, wg-quick determines it automatically from the endpoint addresses or the system default route, which its man page calls usually a sane choice. That is fine on a plain network. It is worth overriding when you know the path is unusual, when you carry the tunnel over another tunnel, or when the endpoint is behind a route you cannot see.

QUICK CHECK

A 1500-byte underlay carries WireGuard over IPv6. What is the largest safe inner MTU?

MSS clamping for TCP

TCP learns packet sizes through the MSS option in the handshake, and relies on ICMP 'fragmentation needed' messages for path MTU discovery. When a firewall eats those ICMP messages, TCP keeps sending segments that never arrive: the hang described above. MSS clamping fixes it by rewriting the MSS in SYN packets that cross the tunnel so both ends never send anything too big. This matters most on a gateway that forwards traffic for other machines.

bash
# iptables
iptables -t mangle -A FORWARD -o wg0 -p tcp --tcp-flags SYN,RST SYN \
  -j TCPMSS --clamp-mss-to-pmtu

# nftables equivalent
nft add table inet mangle
nft add chain inet mangle forward '{ type filter hook forward priority mangle; }'
nft add rule inet mangle forward oifname "wg0" tcp flags syn tcp option maxseg size set rt mtu
Clamp MSS to the tunnel's MTU on a forwarding gateway

Check for UDP buffer drops

WireGuard rides on UDP. If bursts arrive faster than the kernel drains the socket buffer, packets are dropped before WireGuard ever sees them, and TCP inside the tunnel backs off. The kernel counts these in the UDP receive buffer error counter, which is a read-only check.

bash
nstat -az UdpRcvbufErrors UdpSndbufErrors
sysctl net.core.rmem_default net.core.rmem_max net.core.wmem_max

# run a load, then compare
iperf3 -c 10.8.0.1 -t 30
nstat -az UdpRcvbufErrors
Do the drops grow while you run a test?

If the counter climbs during a test, larger limits can help. The defguard WireGuard tuning guide suggests raising rmem_max and wmem_max to roughly 16 MB and rmem_default to 262144 for links above 1 Gbps. Treat those as a starting point and verify with your own counters, because a growing counter is a signal, not proof that the buffer is the only cause.

bash
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.core.rmem_default = 262144
/etc/sysctl.d/90-wireguard.conf (starting point, then measure)
›Why raising rmem_max does not always change anything

A sysctl limit only caps what a socket may request. A userspace program that never asks for a bigger buffer keeps the default, and a socket created inside the kernel does not go through the same path as a normal application. That is why you should always compare the drop counter before and after a change rather than assume the new values apply.

Benchmark it properly

Test the underlay first, then the tunnel, so you know what you are comparing against. Run iperf3 in both directions, because asymmetric links and offloads often make one direction much slower.

bash
# on the server
iperf3 -s

# from the client: underlay (public IP), then tunnel (wg address)
iperf3 -c 203.0.113.10 -t 20
iperf3 -c 203.0.113.10 -t 20 -R
iperf3 -c 10.8.0.1 -t 20
iperf3 -c 10.8.0.1 -t 20 -R

# watch for retransmits (Retr column) and try a few MTUs
sudo ip link set dev wg0 mtu 1380
iperf3 -c 10.8.0.1 -t 20
Underlay versus tunnel

A tunnel that reaches roughly the underlay speed minus a few percent of header overhead is healthy. A tunnel at half the speed with many retransmits points at MTU or MSS. A tunnel that is fast to start and then collapses points at drops.

Gotchas

  • Nested tunnels: WireGuard over another VPN, or over PPPoE, shrinks the budget again. Measure the real path.
  • Mismatched MTUs on the two peers cause one-way trouble. Set both sides deliberately.
  • Do not set the MTU below 1280 if you carry IPv6 inside the tunnel, since 1280 is the IPv6 minimum.
  • Changing the MTU on a live interface only affects new packets. Existing TCP connections keep their old MSS until they reconnect.

Takeaway

WireGuard itself is rarely the bottleneck. Know the 60 or 80 byte overhead, measure the path with ping -M do, set the MTU on purpose, clamp MSS on gateways, and confirm with iperf3 and the UDP error counters. Three measurements beat any amount of folklore.

SOURCES

  1. WireGuard (header layout and MTU notes) — Wikipedia
  2. WireGuard mailing list: MTU discussion by Jason A. Donenfeld, December 2016 — WireGuard mailing list
  3. MTU/MSS Decision Tree for WireGuard Deployments — Defguard
  4. Linux Kernel WireGuard tuning — Defguard
  5. wg-quick(8) manual page — man7.org
  6. Udp_RcvbufErrors: tuning kernel receive buffers — Netdata

Frequently asked questions

What is the best MTU for WireGuard?

1420 is the safe default on a 1500-byte underlay because it works over both IPv4 and IPv6 outer transport. If you are certain the outer transport is IPv4 only, 1440 also fits.

How much overhead does WireGuard add?

60 bytes over IPv4 (20 IP, 8 UDP, 32 WireGuard) and 80 bytes over IPv6 (40 IP, 8 UDP, 32 WireGuard).

Why does SSH connect but hang on large output through my tunnel?

Small packets fit, but full-size TCP segments exceed the path MTU and the ICMP messages that would signal this are blocked. Lower the interface MTU or clamp the MSS on the gateway.

Do I need to tune UDP buffers for WireGuard?

Only if nstat shows UdpRcvbufErrors growing during load. Raise the sysctl limits, then measure again to confirm the change helped.

NEWSLETTER

Get the engineering notes in your inbox.

Kernels, GPUs, networks, AI infrastructure — and the things we broke along the way. No spam, one click to leave.

We store only your email address, to send you new articles. See our privacy policy.

TAGS

WireGuardMTUMSSiperf3LinuxVPNUDP

RELATED

ENGINEERING NOTES

Things we built.Things we broke.Things we learned.