Why a Direct, 500 Mbit Tailscale Link Only Gave Me 10 Mbit for One Stream | LTe - Today I Learned
Table of Contents
The Problem#
My self-hosted Jellyfin was slow whenever I connected to it from abroad over Tailscale. My ISP upload is about 500 Mbit/s, but streams stuttered and iperf3 over the tunnel only managed ~10 Mbit/s:
$ iperf3 -c jellyfin-host<br>[ 5] 0.00-10.00 sec 11.9 MBytes 10.0 Mbit/s<br>It wasn’t a relay or routing issue — tailscale status showed a direct connection (not a DERP relay):
$ tailscale status<br>100.xx.xx.xx jellyfin-host linux active; direct xx.xx.xx.xx:41641<br>So: direct, low-latency, half a gigabit of upload available — and still only 10 Mbit/s for a single stream.
The Cause: Lossy UDP + CUBIC#
Mesh VPNs (Tailscale/WireGuard) wrap all traffic — including TCP — inside UDP . When a lossy mobile/WiFi link drops a UDP packet, the inner TCP sees a gap and the default CUBIC congestion control assumes congestion, halving its window. But the loss is usually random radio loss, not congestion. CUBIC throttles anyway.
A fraction of a percent of loss is enough to wreck it (throughput ∝ 1 / (RTT·√loss), where RTT is the round-trip time). A multi-stream speed test hides this; a single video stream doesn’t. Hence “direct, low-latency, lots of bandwidth, still slow.”
The Fix: BBR#
BBR models bandwidth and RTT directly instead of treating loss as congestion — exactly right for a tunneled, lossy link. Set it on whatever box sends video to the client (Jellyfin server or reverse proxy):
echo "tcp_bbr" | sudo tee /etc/modules-load.d/bbr.conf<br>echo "net.core.default_qdisc=fq" | sudo tee /etc/sysctl.d/99-bbr.conf<br>echo "net.ipv4.tcp_congestion_control=bbr" | sudo tee -a /etc/sysctl.d/99-bbr.conf<br>sudo sysctl --system
Interleaved A/B over cellular (alternating back-to-back on the same path):
cubic: 80 87 80 Mbit/s<br>bbr: 269 248 253 Mbit/s<br>~3x, repeatable. On a clean LAN path BBR shows no gain — there’s no loss to be resilient against, which is why a single before/after on a variable network fooled me at first.
Lessons#
“Direct” ≠ “fast” — link loss still rules.
VPN = TCP-in-UDP ; a little UDP loss kills a single inner stream under CUBIC. BBR is the cure.