Downloading a movie from a self-hosted Jellyfin server worked perfectly at home and failed on every other network. Streaming was fine in both places. Nothing in Jellyfin's logs looked wrong. The cause was a documented Cloudflare limit that most self-hosters never hit, because most self-hosted traffic is small — and the shape of the failure makes it look like an application bug for as long as you're willing to believe it.
Cloudflare's Free plan caps the body of a single proxied response at 100 MB. That's per response, not per month, and it applies to anything going through an orange-clouded DNS record. Ask a proxied origin for a 40 GB file and Cloudflare terminates the connection almost immediately — it doesn't stream 100 MB and stop politely.
This bites self-hosters with:
/Items/{id}/Download serves the full original file.Two things disguise it, and both are worth internalising because they generalise to any CDN-in-front problem:
It works at home. Mobile apps like Jellyfin's typically find the server by LAN discovery or a direct IP when you're on the same network, so they never touch the public hostname and never traverse Cloudflare. The proxied path is only exercised when you're away — which makes it look like a client, ISP or "mobile data" problem rather than infrastructure.
Streaming keeps working. Transcoded and direct-play streaming is delivered as many small segments, each far below the cap. Only the single-response full-file download crosses it. "Playback is fine, downloads are broken" is a genuinely confusing signal, and it points away from the network.
Read the reverse proxy's access log at the origin — Caddy here, but the same fields exist in nginx or Traefik. You are looking for three things together:
/Items/<id>/Download for Jellyfin).Cf-Ray and
Cdn-Loop: cloudflare. That proves the request came through the proxy and not directly,
which is the fact the whole diagnosis turns on.writing: http2: stream closed. A connection that dies that fast on a multi-gigabyte file
never got going; that's not a slow network or a timeout.journalctl -u caddy -f | grep -i download
# or, if you log to a file:
tail -f /var/log/caddy/access.log | grep Download
The detail that rules out the obvious workaround: clients retry with a
Range header to resume, and those retries die just as fast. Cloudflare rejects based on the
advertised remaining length, not on bytes accumulated so far — so "just resume it" cannot get you past a
file that's over the cap, and chunked client-side downloading doesn't rescue it either.
The cap isn't a setting you can raise on any economically sane plan, so the fix is to take that single hostname off Cloudflare's proxy while leaving the rest of your domain alone.
In the Cloudflare dashboard, set that record to DNS only (grey cloud) instead of Proxied (orange cloud). What you give up on that hostname is real and worth stating: origin IP hiding, Cloudflare's DDoS absorption, and its WAF. What you get back is unlimited response sizes.
If hiding your home IP was the reason for proxying in the first place, point the DNS-only record at a cheap VPS and forward the traffic — a raw TCP passthrough on 80/443 to your reverse proxy at home keeps the origin hidden without a CDN in the response path. Two things to plan for if you do that:
Cf-Ray
present or absent immediately tells you which path a request took, which is the fastest way to separate
"client problem" from "proxy problem".