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 SQLite database, resolve linked 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.
- Jellyfin Web: Stalls for tens of seconds when browsing playlist views.
- 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 standard Nginx setups) 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.
If your music library is mounted from an exFAT, NTFS, or SMB drive, filesystem path matching is case-insensitive on disk. However, Jellyfin's SQLite database performs exact case-sensitive matching against paths in its BaseItems table.
- When an M3U playlist contains track paths with casing discrepancies (e.g.
LosslessvsLOSSLESS,Various ArtistsvsVarious artists,.flacvs.FLAC), Jellyfin fails to match them:[WRN] Unable to find linked item at path "/mnt/user/music/..." - The song silently disappears from the playlist in Jellyfin and all connected players (WiiM, Feishin, Web).
- Solution: Use
tools/fix-m3u-case.pyto automatically synchronize M3U playlist file paths with the canonical database casing stored in Jellyfin'sjellyfin.db.
.
βββ nginx-jellyfin-cache.conf # Nginx site configuration (REST & SOAP cache)
βββ jellyfin-cache-prewarm.py # Python script for DLNA, REST & Web UI pre-warming
βββ jellyfin-proxy-ssdp.py # Standalone SSDP advertiser for port 8096 proxy
βββ tools/
β βββ fix-m3u-case.py # Fixes playlist path casing against jellyfin.db
βββ systemd/
βββ jellyfin-cache-prewarm.service # Systemd oneshot warmup service
βββ jellyfin-cache-prewarm.timer # Systemd timer (runs every 4 hours)
βββ jellyfin-proxy-ssdp.service # Systemd service for SSDP advertiser
flowchart LR
subgraph Clients
WiiM["WiiM Ultra / UPnP"]
Feishin["Feishin / Finamp"]
Web["Jellyfin Web"]
end
subgraph Server["Jellyfin Host"]
SSDP["SSDP Advertiser (:1900 UDP)"]
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
SSDP -.->|Advertises :8096| WiiM
Nginx <--> Cache
Nginx -->|Proxy Pass| Jellyfin
- Frontend Nginx: Listens on
:80and:8096. - Jellyfin Origin: Rebound to
:8095(localhost only). - Endpoints Cached:
/playlists/{id}/items(Feishin)/users/{uid}/items/{id}(Feishin metadata)/items?ParentId=...(Finamp)/users/{uid}/items?ParentId=...(Jellyfin Web & generic clients)/artists,/artists/albumartists,/musicgenres/dlna/{id}/contentdirectory/control(SOAP Browse)/jellyfin-proxy/description.xml(Custom UPnP device description)
- 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 & Folders for all users:
- Playlists folder view & metadata (
/Users/{uid}/Items?ParentId={playlists_folder_id}&StartIndex=0&Limit=100...used by Web and Smart TVs like LG webOS) - Feishin items & metadata endpoints
- Finamp items endpoint
- Jellyfin Web client items endpoint (
/Users/{uid}/Items?ParentId=...&Limit=300)
- Playlists folder view & metadata (
-
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 (JELLYFIN_API_TOKEN):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
If songs are missing from your M3U playlists in Jellyfin due to exFAT/Samba case differences:
python3 tools/fix-m3u-case.py /path/to/playlist.m3u /var/lib/jellyfin/data/jellyfin.dbThe script will match each track path case-insensitively against Jellyfin's database, create a backup copy (.casefix_bak), and rewrite the playlist with the exact canonical casing.
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!