High-performance Nginx caching reverse proxy and automated pre-warming suite specifically designed for Jellyfin Media Server, optimized for:
- Massive Music Libraries & Playlists (5,000+ tracks per playlist).
- UPnP / DLNA Streamers (WiiM Ultra, WiiM Pro, Yamaha MusicCast, Cambridge Audio, etc.).
- Modern Music Clients (Feishin, Finamp, Jellyfin Web).
- Eliminating Cloudflare 524 Timeouts & UPnP Device Hangs.
When requesting large playlists (e.g. 4,000–6,000 songs) in Jellyfin, the server must query the database, parse tracks, format metadata, and serialize response payloads up to 10MB in size.
- Without Cache: Takes 30 to 180 seconds per request.
- Client Impact:
- Cloudflare: Drops connections after 100s with
HTTP 524 Gateway Timeout. - Feishin / Finamp: Shows spinning wheel, infinite loaders, or crashes.
- UPnP / DLNA Streamers: Network timeouts (
HTTP 504), socket disconnections, or empty playlist views.
- Cloudflare: Drops connections after 100s with
- With Cache: Cached responses are served in under 10ms, eliminating server load completely.
DLNA ContentDirectory browse requests use HTTP POST with XML/SOAP envelopes (SOAPACTION: "urn:schemas-upnp-org:service:ContentDirectory:1#Browse").
- By default, HTTP caches (including Nginx) only cache
GETandHEADrequests. - Nginx here is uniquely configured to cache
POSTrequests keyed bySOAP|$uri|$http_soapaction|$request_body.
In Jellyfin, DLNA DIDL-Lite metadata dynamically includes track <res> and <upnp:albumArtURI> URLs using the incoming HTTP Host header.
- If a background warmup script or localhost crawler queries
http://127.0.0.1, Jellyfin generates URLs likehttp://127.0.0.1/dlna/audio/.../stream.flac. - If saved to cache, external DLNA renderers (like a physical WiiM Ultra streamer) receive
127.0.0.1and try to fetch streams and album art from themselves, causing playback failure and missing cover art. - Solution: The Nginx configuration enforces the server's real LAN IP and port in
proxy_set_header Host "<SERVER_IP>:<PORT>"and performs response body rewriting (sub_filter).
When Nginx serves a cached 200 OK response directly from disk, Jellyfin's upstream CORS headers (Access-Control-Allow-Origin, etc.) are missing.
- Web apps and desktop clients (Feishin) reject the response due to browser CORS policies.
- Solution: Nginx strips upstream CORS headers and injects uniform CORS headers for all responses.
.
├── nginx-jellyfin-cache.conf # Nginx site configuration
├── jellyfin-cache-prewarm.py # Python script for DLNA & REST pre-warming
└── systemd/
├── jellyfin-cache-prewarm.service # Systemd oneshot service
└── jellyfin-cache-prewarm.timer # Systemd timer (runs every 4 hours)
flowchart LR
subgraph Clients
WiiM["WiiM Ultra / UPnP"]
Feishin["Feishin / Finamp"]
Web["Jellyfin Web"]
end
subgraph Server["Jellyfin Host"]
Nginx["Nginx Cache Proxy (:80 / :8096)"]
Cache[("/var/cache/nginx/jellyfin")]
Jellyfin["Jellyfin Backend (:8095)"]
end
WiiM -->|Browse / Stream| Nginx
Feishin -->|REST API| Nginx
Web -->|UI / WebSockets| Nginx
Nginx <--> Cache
Nginx -->|Proxy Pass| Jellyfin
- Frontend Nginx: Listens on
:80and:8096. - Jellyfin Origin: Rebound to
:8095(localhost only). - Endpoints Cached:
/playlists/{id}/items/users/{uid}/items/{id}/items?ParentId=.../users/{uid}/items?ParentId=.../artists,/artists/albumartists,/musicgenres/dlna/{id}/contentdirectory/control(SOAP Browse)
- Nginx with
http_sub_module(standard in Ubuntu/Debian:apt install nginx). - Python 3.8+ (standard library only, no external pip dependencies).
- Jellyfin Media Server.
Change Jellyfin's internal HTTP port from 8096 to 8095:
In Jellyfin Dashboard -> Networking -> Local HTTP port: change 8096 to 8095, or edit /etc/jellyfin/network.xml:
<LocalHttpPort>8095</LocalHttpPort>Restart Jellyfin:
sudo systemctl restart jellyfin- Copy the configuration file:
sudo cp nginx-jellyfin-cache.conf /etc/nginx/sites-available/jellyfin-cache.conf
- Edit
/etc/nginx/sites-available/jellyfin-cache.confand update your server's reachable LAN IP:# Replace 192.168.2.251 with your server's LAN IP: proxy_set_header Host "192.168.2.251:8096"; sub_filter "http://127.0.0.1/" "http://192.168.2.251:8096/"; sub_filter "http://192.168.2.251/" "http://192.168.2.251:8096/";
- Enable the site and create the cache directory:
sudo mkdir -p /var/cache/nginx/jellyfin sudo chown -R www-data:www-data /var/cache/nginx/jellyfin sudo ln -sf /etc/nginx/sites-available/jellyfin-cache.conf /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx
The pre-warm script iterates over:
-
DLNA Root (
ObjectID: 0) and Playlists folder. -
DLNA pagination chunks (
RequestedCount: 10, 100, 500) to guarantee that mobile UPnP apps (like WiiM Home) immediately hit the cache when opening playlists. -
REST API Playlists for all users (Feishin and Finamp endpoints).
-
Copy the script to
/usr/local/bin:sudo cp jellyfin-cache-prewarm.py /usr/local/bin/jellyfin-cache-prewarm.py sudo chmod +x /usr/local/bin/jellyfin-cache-prewarm.py
-
Generate an API Key in Jellyfin:
- Dashboard -> Administration -> API Keys -> Create new key (e.g.
CachePrewarm).
- Dashboard -> Administration -> API Keys -> Create new key (e.g.
-
Set your configuration inside
/usr/local/bin/jellyfin-cache-prewarm.pyor via environment variables:API_TOKEN = "your_jellyfin_api_key_here"
-
Test run the script manually:
sudo /usr/local/bin/jellyfin-cache-prewarm.py
To keep the cache continuously fresh without manual intervention:
- Copy systemd units:
sudo cp systemd/jellyfin-cache-prewarm.service /etc/systemd/system/ sudo cp systemd/jellyfin-cache-prewarm.timer /etc/systemd/system/
- Reload and enable the timer:
sudo systemctl daemon-reload sudo systemctl enable --now jellyfin-cache-prewarm.timer - Verify status:
systemctl list-timers | grep jellyfin
Check real-time cache hits and misses:
tail -f /var/log/nginx/cache.logSample output:
2026-09-18T16:24:41 client=192.168.2.96 cache=HIT status=200 rt=0.001 "/dlna/.../control" action="urn:schemas-upnp-org:service:ContentDirectory:1#Browse"
2026-09-18T16:24:42 client=192.168.2.250 cache=HIT status=200 rt=0.012 "/playlists/.../items" action="-"
To purge the cache completely:
sudo rm -rf /var/cache/nginx/jellyfin/* && sudo systemctl reload nginxMIT License. Feel free to use and contribute!