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
[ 5] 0.00-10.00 sec 11.9 MBytes 10.0 Mbit/s
It wasn’t a relay or routing issue — tailscale status showed a direct connection (not a DERP relay):
$ tailscale status
100.xx.xx.xx jellyfin-host linux active; direct xx.xx.xx.xx:41641
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
echo "net.core.default_qdisc=fq" | sudo tee /etc/sysctl.d/99-bbr.conf
echo "net.ipv4.tcp_congestion_control=bbr" | sudo tee -a /etc/sysctl.d/99-bbr.conf
sudo sysctl --system
Interleaved A/B over cellular (alternating back-to-back on the same path):
cubic: 80 87 80 Mbit/s
bbr: 269 248 253 Mbit/s
~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.