WireGuard Performance Tuning on a VPS: MTU, Multicore and Throughput Measurement

WireGuard is fast by default, but “fast by default” is not the same as “fast on your VPS”. A single-threaded tunnel on a small instance can saturate at a few hundred megabits, and the wrong MTU will silently fragment every large packet and halve your real file-transfer speed. This guide tunes WireGuard for throughput and latency on a small VPS, and shows the exact commands to measure whether each change helped.

WireGuard belongs in the same toolbox as the tuning topics in our VPS performance tuning guides: you change one parameter, measure, and keep the change only if the number moves. Networking is unforgiving of folklore.

Establish the Baseline Before Tuning Anything

You cannot improve what you have not measured. Set up the tunnel, then run an iperf3 test through it, plus a latency distribution, and record both. Everything later is compared against these numbers.

ip -br addr show wg0
wg show

# baseline throughput through the tunnel (server side: iperf3 -s)
iperf3 -c 10.0.0.1 -t 30 -P 4

# latency distribution
for i in $(seq 1 100); do ping -c1 -W1 10.0.0.1 | tail -1 | awk '{print $4}' | cut -d/ -f2; done | sort -n | awk '{a[NR]=$1} END{print "p50",a[int(NR*0.5)],"p99",a[int(NR*0.99)]}'

Also check the cost of the cryptography itself. On a modern x86 VPS, ChaCha20-Poly1305 runs at multiple gigabytes per second per core, but on a tiny ARM or an older core it can become the bottleneck. perf top during a transfer tells you immediately whether you are spending CPU in chacha20 or in the network stack.

Fix the MTU — the Most Common Silent Killer

WireGuard adds 60 bytes of header overhead over IPv4 and 80 over IPv6. If your underlying path MTU is 1500, the tunnel interface must be no larger than 1420 for IPv4. Get this wrong and large packets fragment or, worse, get dropped entirely — small pings succeed while bulk transfers stall, which sends people chasing phantom bugs.

ip link set dev wg0 mtu 1420
ip -br link show wg0

# verify no fragmentation on a large packet
ping -M do -s 1392 -c 3 10.0.0.1
  • IPv4 underlay: set wg0 MTU to 1420
  • IPv6 underlay: set wg0 MTU to 1400 to account for the larger header
  • Path MTU clamping: ensure net.ipv4.tcp_mtu_probing=1 as a fallback
  • Test with ping -M do at the largest size that succeeds without fragmentation

If the tunnel crosses a network that itself uses PPPoE or another encapsulation, the underlay MTU is already below 1500 and you must subtract the overhead from that smaller figure. Measure the underlay MTU with a large ping -M do to the peer’s real IP before you decide the tunnel MTU.

Spread the CPU Load Across Cores

WireGuard processes each peer’s traffic on the CPU core that handles the incoming packet. On a single-flow tunnel you are limited to one core. If you need more throughput, use multiple parallel flows, which the kernel can schedule across cores, or accept the single-core ceiling.

# watch per-core utilisation during a transfer
mpstat -P ALL 1

# parallel flows spread across cores
iperf3 -c 10.0.0.1 -P 8 -t 30

Confirm the scaling: run the same transfer at -P 1 and -P 8 and compare aggregate throughput. If throughput scales, your tunnel was core-bound and parallel flows help. If it flattens immediately, the bottleneck is elsewhere — likely the underlying NIC or the provider’s uplink.

Tune the Queue Discipline and Buffer Sizing

WireGuard interfaces benefit from a modern queueing discipline to keep bufferbloat down and latency tight. Apply fq_codel or cake to the tunnel interface, and match your socket buffers to the bandwidth-delay product so the kernel neither starves nor over-buffers the connection.

tc qdisc replace dev wg0 root fq_codel
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216
sysctl -w net.ipv4.tcp_rmem="4096 87380 16777216"
sysctl -w net.ipv4.tcp_wmem="4096 65536 16777216"

Then re-measure latency under load — this is the point of the exercise. Run the latency loop while simultaneously running iperf3, and check that p99 stays low. If it climbs by hundreds of milliseconds, your queue discipline is wrong or your buffers are oversized. Throughput without low latency is a half-solution.

For long-haul or lossy links, WireGuard’s own automatic receive window and adaptive send rate do a good job, but the fq_codel on the tunnel keeps the underlying path’s queues from growing without bound. The combination of a correct MTU and a fair queue discipline fixes more real-world tunnel complaints than any kernel tweak.

Persistent Keepalive and NAT Traversal Costs

If either peer is behind NAT, you need PersistentKeepalive to hold the mapping open, but a short interval generates constant traffic and wastes a wakeup per interval. Twenty-five seconds is the canonical value and is right for most cases; do not set it to one second “for reliability”.

[Peer]
PublicKey = ...
Endpoint = peer.example.com:51820
AllowedIPs = 10.0.0.0/24
PersistentKeepalive = 25

On mobile or metered links, keepalive costs battery and data. Tune it to the shortest interval your NAT timeout requires and no shorter. Check the NAT timeout empirically by watching handshake timestamps in wg show; if the tunnel survives on 25 seconds, leave it.

Verify the Cryptography Is Not Your Bottleneck

Finally, confirm that CPU spent on ChaCha20-Poly1305 is not the limiting factor. Modern x86 and ARM cores include AES acceleration and vector instructions that WireGuard uses; a very small or old VPS may not, and the crypto cost becomes visible.

perf stat -e cycles,instructions -p $(pgrep -f 'wireguard' | head -1) sleep 10
perf top -C 0 --sort cpu

If crypto dominates, consider running WireGuard on a slightly larger instance — the same “benchmark before you commit” logic that underpins our VPS comparison table on the main site. Getting the instance size right is cheaper than fighting the crypto ceiling with settings.

Summary: Measure, Change One Thing, Keep the Wins

WireGuard tuning on a VPS comes down to four levers: the correct tunnel MTU, enough CPU to absorb both crypto and packet processing, a fair queue discipline for low latency under load, and sane keepalive for NAT. Fix the MTU first — it is the most common and most damaging mistake. Then parallelise flows if you are core-bound, then tune queues and buffers.

Measure after every single change and keep only what moves the number. Record your baseline and your results so future kernel or provider changes can be compared against them. That habit, more than any individual setting, is what keeps a tunnel fast over time.

Leave a Reply