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 most self-hosters never hit, because most self-hosted traffic is small. The shape of the failure makes it look like an application bug for as long as you are willing to believe that.
Cloudflare's Free plan caps the body of a single proxied response at 100 MB. That is 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 does not stream 100 MB and stop politely.
This bites self-hosters with:
/Items/{id}/Download serves the full original file.Two things disguise it. Both are worth internalising, because they generalise to any CDN-in-front problem:
It works at home. Mobile apps like Jellyfin's find the server by LAN discovery or a direct IP when you are on the same network. They never touch the public hostname and never traverse Cloudflare. The proxied path is only exercised when you are away, which makes it look like a client, ISP or mobile-data problem rather than infrastructure.
Streaming keeps working. Transcoded and direct-play streaming arrives 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, though the same fields exist in nginx or Traefik. You want three things together:
/Items/<id>/Download for Jellyfin.Cf-Ray and
Cdn-Loop: cloudflare. Those prove 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 is 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
One detail rules out the obvious workaround. Clients retry with a Range header
to resume, and those retries die just as fast. Cloudflare rejects on the advertised remaining length, not on
bytes accumulated so far. So "just resume it" cannot get you past a file over the cap, and chunked
client-side downloading does not rescue it either.
The cap is not a setting you can raise on any economically sane plan. Take that single hostname off Cloudflare's proxy and leave the rest of your domain alone.
In the Cloudflare dashboard, set that record to DNS only, the grey cloud, instead of Proxied, the orange one. What you give up on that hostname is real and worth stating. Origin IP hiding, Cloudflare's DDoS absorption, its WAF. What you get back is unlimited response sizes.
If hiding your home IP was the reason for proxying, point the DNS-only record at a cheap VPS and forward the traffic. A raw TCP passthrough on 80 and 443 to your reverse proxy at home keeps the origin hidden with no CDN in the response path. Plan for two things if you do:
Cf-Ray present
or absent tells you which path a request took immediately. That is the fastest way to separate "client
problem" from "proxy problem".