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.