Get started
Networking

Understanding TCP vs UDP Tunneling Overhead for Low-Latency Applications

Where tunnel overhead actually comes from: header bytes worked out from first principles, MTU and MSS arithmetic, and the TCP-over-TCP and head-of-line effects that cost far more latency than the bytes do.

Every tunnel wraps one packet inside another, and the wrapping has a price. Some of that price is bytes: extra headers on every packet, which matter most when packets are small and frequent, as they are in games, voice and real-time APIs. The larger part, for latency-sensitive traffic, is behaviour: what the tunnel does when a packet is lost. A tunnel that runs over TCP can turn a single lost packet into a stall for every flow inside it.

This guide works through both. All overhead figures are derived from header sizes, with the arithmetic shown, so you can redo it for your own stack.

Key takeaways

  • On a 1500-byte path, a WireGuard tunnel costs 60 bytes per packet over IPv4 and 80 over IPv6, plus up to 15 bytes of padding. That is 4 to 5.3 percent of a full-size packet, but it can make a small packet more than half as large again.
  • MTU problems, not raw overhead, cause most "the tunnel is slow" reports: fragmented or silently dropped large packets look like random stalls.
  • Running TCP inside a TCP tunnel stacks two sets of retransmission timers on top of each other. Under loss they interact badly, which is why it is called TCP meltdown.
  • Any tunnel that carries several flows over one TCP connection adds head-of-line blocking: one lost segment delays every flow behind it.
  • For low-latency traffic, use a UDP-based tunnel where you can, and keep TCP-based tunnelling as a fallback for networks that block UDP.

The header sizes

These are the building blocks. The MTU (maximum transmission unit) counts from the start of the IP header, so the Ethernet header and frame check sequence sit outside the 1500-byte figure.

LayerSize in bytesNotes
IPv4 header20Up to 60 with options; options are rare on the public internet
IPv6 header40Fixed; extension headers add more
UDP header8Fixed
TCP header20Minimum; up to 60 with options
TCP header with timestamps3220 plus a 10-byte timestamp option and 2 bytes of padding; Linux enables timestamps by default
WireGuard data message3216-byte header (type, reserved, receiver index, 8-byte counter) plus a 16-byte Poly1305 tag
TLS 1.3 record225-byte record header, 1-byte inner content type, 16-byte AEAD tag; one record can carry many packets
VXLAN50Inner Ethernet 14, VXLAN 8, outer UDP 8, outer IPv4 20

WireGuard also pads the encrypted payload to a multiple of 16 bytes, as long as that does not exceed the tunnel MTU. A 118-byte inner packet is sent as 128 bytes of ciphertext plus the 32-byte header and tag. So WireGuard's per-packet overhead ranges from 32 to 47 bytes before the outer headers.

MTU and MSS arithmetic

Without a tunnel

The largest TCP payload in one segment, the MSS (maximum segment size), is the MTU minus the IP and TCP headers:

IPv4: 1500 - 20 (IPv4) - 20 (TCP) = 1460 bytes MSS
IPv6: 1500 - 40 (IPv6) - 20 (TCP) = 1440 bytes MSS

With timestamps, the payload actually carried per IPv4 segment:
      1460 - 12 (timestamp option) = 1448 bytes

With WireGuard

The tunnel interface's MTU is the path MTU minus everything the tunnel adds:

Outer IPv4: 1500 - 20 (IPv4) - 8 (UDP) - 32 (WireGuard) = 1440
Outer IPv6: 1500 - 40 (IPv6) - 8 (UDP) - 32 (WireGuard) = 1420

wg-quick defaults to 1420 when you do not set an MTU, because it subtracts the IPv6 worst case of 80 bytes. Inside that tunnel, TCP's MSS shrinks again:

Inner IPv4 over a 1420 tunnel: 1420 - 20 - 20 = 1380 bytes MSS
Inner IPv6 over a 1420 tunnel: 1420 - 40 - 20 = 1360 bytes MSS

If the outer path is itself smaller than 1500, as it is on PPPoE links (1492) or some mobile networks, subtract from that figure instead. On PPPoE with an IPv4 outer header, the tunnel MTU becomes 1492 - 60 = 1432.

What the overhead costs: two example calculations

For a full-size packet, WireGuard over IPv4 adds 60 bytes to 1440, so the share of the wire spent on tunnel headers is:

60 / 1500 = 4.0 percent

Now take a small, frequent packet, for instance a game state update with 90 bytes of application data sent over UDP every 20 milliseconds (50 packets per second):

Plain:     90 data + 8 UDP + 20 IPv4               = 118 bytes
Tunnelled: 118 inner, padded to 128
           + 32 WireGuard + 8 UDP + 20 IPv4        = 188 bytes

Overhead:  (188 - 118) / 118                       = 59 percent more bytes
Bandwidth: 118 x 8 x 50 = 47,200 bit/s  (47.2 kbit/s)
           188 x 8 x 50 = 75,200 bit/s  (75.2 kbit/s)

Those are IP-layer figures; link-layer framing adds more on top of both. The point stands either way: on small packets, tunnel headers can be most of the cost.

Do the extra bytes add latency?

Only a little, through serialization delay, the time to clock bits onto a link. An example calculation for the 60 extra bytes:

At 10 Mbit/s:   60 x 8 / 10,000,000    = 48 microseconds
At 1 Gbit/s:    60 x 8 / 1,000,000,000 = 0.48 microseconds

That is rarely what users notice. Two things usually matter far more: the route, and loss recovery. If the tunnel endpoint is off the direct path, every packet makes a detour. Light in optical fibre travels at roughly 200,000 km per second, about 5 microseconds per kilometre, so a detour that adds 1,000 km each way adds:

1,000 km x 5 us/km x 2 directions = 10 ms of round-trip time

Place tunnel endpoints close to the path between your users and your servers, and loss recovery becomes the remaining question.

Why TCP tunnels hurt real-time traffic

TCP over TCP: meltdown

When a tunnel carries IP packets over a TCP connection (OpenVPN in TCP mode, PPP over SSH, or ssh -w tunnel devices), the inner traffic is often TCP too. Now two reliable transports are stacked:

  1. A packet is lost on the path. The outer TCP connection notices and retransmits it, after at least one round trip.
  2. The outer connection delivers bytes strictly in order, so everything behind the lost segment waits.
  3. The inner TCP connection sees no loss, only a sudden delay. If the delay exceeds its retransmission timeout, it retransmits too, even though the outer layer is already recovering the original.
  4. The inner retransmissions travel through the outer connection, which is already congested and backed off, adding more queued data.
  5. Both layers back off their timers. Under sustained loss, throughput collapses and latency climbs far beyond what either layer would cause alone.

The effect was described in the early 2000s and has not changed, because it follows from how TCP's timers work: each layer assumes it alone is responsible for reliability.

Head-of-line blocking

Even without meltdown, one TCP connection delivers one ordered byte stream. If a tunnel multiplexes several flows over it, a single lost segment blocks all of them until the retransmission arrives. A voice packet that was never lost still arrives late, because it was queued behind one that was.

How long that wait lasts depends on how the loss is detected:

  • Fast retransmit fires after three duplicate acknowledgements. On a busy connection that takes about one round trip.
  • On a sparse stream, such as 20 packets per second, there may not be three later segments in flight. Recovery then waits on a timer: Linux's tail loss probe, typically around two smoothed round-trip times, or the retransmission timeout, which Linux clamps to a minimum of 200 ms by default.

An example calculation: with a 40 ms round trip, a loss recovered by fast retransmit stalls the stream for roughly 40 ms plus the time for the duplicate acknowledgements to arrive. A loss that falls back to the minimum retransmission timeout stalls it for at least 200 ms, five times the round trip. For a game or a call, that is a visible freeze.

Why UDP tunnels avoid this

WireGuard, and OpenVPN or IPsec in UDP mode, send each inner packet as its own datagram. A lost outer datagram loses exactly one inner packet:

  • An inner UDP application, such as a game or voice codec, treats it as a lost packet and moves on, which is what it was designed to do.
  • An inner TCP connection recovers on its own, once, with its own timers.
  • Other flows in the tunnel are not delayed at all.

SSH port forwarding is a special case

ssh -L and ssh -R do not carry packets. The SSH client terminates the local TCP connection and forwards the byte stream inside an SSH channel. There is only one TCP connection on the path, so there is no TCP-over-TCP meltdown. All channels still share that one connection, though, so head-of-line blocking still applies between them. ssh -w, which creates a tunnel device and carries IP packets, does stack TCP over TCP.

Fixing MTU problems

Measure the path

Send pings with the "do not fragment" bit set and find the largest that passes. The ping payload size excludes the 8-byte ICMP header and the IP header:

# IPv4: 1472 + 8 (ICMP) + 20 (IPv4) = 1500
ping -M do -s 1472 -c 3 198.51.100.1

# IPv6: 1452 + 8 (ICMPv6) + 40 (IPv6) = 1500
ping -6 -M do -s 1452 -c 3 2001:db8::1

# Through a WireGuard tunnel with MTU 1420, IPv4 inside:
# 1392 + 8 + 20 = 1420
ping -M do -s 1392 -c 3 192.0.2.1

These are the Linux iputils options; macOS and BSD use -D for "do not fragment" instead. tracepath 198.51.100.1 reports the path MTU hop by hop without needing root.

Set the tunnel MTU and clamp MSS

Set the MTU on the tunnel interface explicitly, either with MTU = 1420 in the [Interface] section of a wg-quick file or directly:

sudo ip link set dev wg0 mtu 1420

On a gateway that routes traffic for other machines, clamp the MSS of forwarded TCP connections, so hosts behind it never send segments too large for the tunnel. With nftables:

table inet mangle {
    chain forward {
        type filter hook forward priority mangle; policy accept;
        oifname "wg0" tcp flags syn tcp option maxseg size set rt mtu
    }
}

rt mtu takes the value from the route, so it follows the tunnel MTU if you change it. The iptables equivalent is:

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

Do not block the ICMP that path MTU discovery needs

Path MTU discovery relies on routers returning ICMPv4 "fragmentation needed" (type 3, code 4) or ICMPv6 "packet too big" (type 2). A firewall that drops all ICMP creates a black hole: the handshake succeeds, small requests work, and large responses hang forever. Allow those messages through. As a safety net on Linux servers, net.ipv4.tcp_mtu_probing = 1 makes TCP probe for a smaller segment size when it detects a black hole.

When you cannot avoid TCP

Some networks block UDP outright, and a TCP-based tunnel on port 443 is the only thing that gets through. You can still limit the damage:

  • Try UDP first and fall back. Many clients can attempt a UDP transport and switch to TCP only when it fails.
  • Forward streams, not packets. Where possible, use stream forwarding (like ssh -L) instead of carrying IP packets, which removes the meltdown case.
  • Keep queues short. Large send buffers on the tunnel connection hide loss as latency. On Linux, TCP_NOTSENT_LOWAT (socket option, or the net.ipv4.tcp_notsent_lowat sysctl) limits unsent data buffered in the kernel; set TCP_NODELAY so small writes are not delayed by Nagle's algorithm.
  • Separate real-time flows. If a latency-sensitive flow must share a TCP tunnel, give it its own connection so it does not wait behind bulk transfers.
  • Consider QUIC-based tunnelling. Proxying UDP in HTTP (RFC 9298, part of the MASQUE work) can carry UDP payloads in QUIC datagrams over UDP, so a lost packet is not retransmitted by the tunnel and does not hold up other traffic. It still needs UDP on the path, and support in clients and proxies varies; check what your tools implement.

Failure modes and troubleshooting

SymptomLikely causeWhat to check
SSH works, large web pages or git clone hangMTU black holeping -M do at decreasing sizes; allow ICMP "packet too big"; clamp MSS
Throughput fine on a clean link, collapses with slight lossTCP over TCPIs the tunnel in TCP mode? Switch to UDP if the network allows
Voice or game freezes in bursts, then catches upHead-of-line blocking in a TCP tunnelLoss on the path; move the flow to a UDP tunnel or its own connection
Latency higher than expected, steadyRoute detour through the tunnel endpointmtr with and without the tunnel; compare round-trip times
Latency rises under loadLarge buffers in the tunnel or on the linkQueue sizes, TCP_NOTSENT_LOWAT, and whether the link is saturated
Bandwidth bill higher than traffic suggestsSmall packets with proportionally large headersPacket size distribution; batch small messages where the application allows

Checklist

  • Work out the tunnel MTU from the real path MTU, not an assumed 1500.
  • Set the tunnel MTU explicitly and clamp MSS on any gateway that forwards TCP.
  • Allow ICMPv4 type 3 code 4 and ICMPv6 type 2 through every firewall on the path.
  • Use a UDP-based tunnel for real-time traffic; keep TCP tunnelling as a fallback.
  • Avoid carrying TCP packets inside a TCP tunnel; forward streams instead where you must use TCP.
  • Place tunnel endpoints near the direct path between users and servers.
  • Measure with ping -M do, tracepath and mtr before and after each change.

Closing

Tunnel overhead is easy to calculate and usually small for bulk traffic. What hurts low-latency applications is a wrong MTU, a detour, or a reliable transport stacked on top of another one. Get those right and the headers become a rounding error, except for small packets, where the arithmetic above tells you exactly what you are paying.

To see how your own site responds from the outside, run a speed audit.