Why a Cloudflare Tunnel is not a fire hose for Jellyfin
Building the lab: a side story. (Not part of the numbered series.) ← Two tunnels are better than one · Observability is the part I’m good at
I did a stupid thing, and I am writing it down because the stupid thing had a useful lesson inside it. That usually happens.
I tried to stream Jellyfin through my Cloudflare tunnel: through the front door I built so that nothing would ever be open directly, and through Zero Trust, with a login check in front of it.
It buffered. Of course it buffered. I just did not know why for a very long time.
The tour of wrong answers
The video stuttered. So I did what I always do: I blamed the box.
The server. Wrong.
Jellyfin converts video while you watch, so I assumed the little i5 could not keep up. I checked the stats. It was converting at about 280 frames per second, about ten times real time, and it was a third of the way ahead of me. The server was bored. Not it.
The disk. Also wrong.
The media storage is a mix of disks and a network share, and I had trouble with it before. So I checked. Reads came back at about 300 MB/s. Also bored.
The throttle. Still wrong (but a fun detour).
Earlier, I had turned on a power-saving setting that pauses converting. Maybe it was pausing and starving the player? I turned it off. It did not help.
My internet. Very, very wrong.
Now the only suspect left was my connection. So I ran a speed test, ready to sigh.
Ping 30.8 ms
Download 218 Mbit/s
Upload 450 Mbit/sThe upload was 450. Four hundred and fifty. My connection is a fire hose, and ten megabits of video would not trouble it. So it was not my internet either, and now I had ruled out everything except one thing.
The real problem: the path
Here is the clue that broke it open. I looked at where the player’s requests were coming from. They came from a pod’s address inside the cluster (the ingress), not from my laptop on the same network. So the stream was not going laptop -> server. It was going:
laptop -> my ISP -> Cloudflare edge -> back down the tunnel -> ingress -> JellyfinA video from the same room, taking a trip through the internet and back. Because I had opened the domain name instead of the IP address.
And then the second, funnier problem
Fine, I thought. It is a detour, but a fast one. Then I read the part of Cloudflare’s rules that I had skipped. The free plan is explicitly not for “serving video or a disproportionate percentage of … large files”. They do not give you a meter and point at a number. They just do not want to be your media server, and they are allowed to make that felt.
There is no switch that says “throttle this”. There is no setting I missed. The “throttle” is the rule. I went looking for a number to turn down, and I found a paragraph instead.
And on top of that, every three-second piece of video had to pass through Cloudflare Access: a full login check for every piece, for a movie. I had built a bouncer for a cinema.
The fix, which took one second
I opened Jellyfin at its local address instead (http://192.168.2.81:8096), and it was instant. No tunnel, no edge, no login check for every piece of video. Just a wire between two machines that sit in the same room.
For watching away from home, the right answer is a VPN (I chose Tailscale). Then the video goes straight to the device, and Cloudflare never sees a single frame of it. The tunnel is for the door: the dashboards, the APIs, the login pages. It is not, and never was, a fire hose.
The lesson
I keep learning the same shape of thing. The tunnel is a great tool for exactly what it is for, and I asked it to be something it is not. Then I spent an evening carefully measuring every part of the house except the one that was broken.
“Use the right path for the job” sounds so obvious that it is useless advice. Except I had a 450-megabit upload and a server that was bored, and I still chose the slow way. Because the slow way had a nice lock on it. The lock looked safe, so I held the door open and waved it through.
Building the lab: a side story. ← Two tunnels are better than one · Observability is the part I’m good at