ARRFLIX/docs/23-arrflix-edge-perf-audit.md
s8n 851f317dbb doc 23: arrflix edge / network / browser-load-path perf audit (read-only)
Edge audit complementing doc 13 (server-side perf). Confirms cold-load
"feels slow" perception is dominated by:
  - no HTTP compression at Traefik (2.74 MiB raw JS bundles per cold load)
  - no Cache-Control on hashed-asset URLs (28 conditional GETs per warm load)
  - first-fetch poster image transcode ~385 ms (server-side, doc 13 #02)

TLS, MTU, HTTP/2, cert chain, middleware chain, Pi-hole hairpin all
audited and clean. Pi-hole missing local DNS rewrite for arrflix.s8n.ru
(LAN clients hairpin via WAN unless /etc/hosts pin in place).

Top quick win: add `compress@file` middleware in
/opt/docker/traefik/config/dynamic.yml + reference from Jellyfin router
label. ~70 % cold-load wire-size reduction (2.74 MiB to ~0.82 MiB
gzip / ~0.69 MiB brotli). One file edit, no architectural change.

No fixes applied. No state mutated. No Traefik reload.
2026-05-08 17:50:52 +01:00

25 KiB
Raw Blame History

23 — ARRFLIX Edge / Network / Browser-Load-Path Audit

Status: read-only audit, executed 2026-05-08 from onyx (192.168.0.6 LAN) against https://arrflix.s8n.ru (Jellyfin 10.10.3 behind Traefik on nullstone). Scope: edge — DNS, TLS, Traefik, compression, cache headers, asset waterfall, ServiceWorker. No fixes applied. No state mutated. No container restart. No Traefik reload.

Sibling audits cover color/HDR, server runtime, and storage. This one is the edge slice only. Pairs with doc 13 (server-side optimization audit, 2026-05-08) — that one calls out CPU/transcode; this one identifies why every page-nav over WAN feels gluey when the server is idle.


1. Executive summary

The two cold-load complaints ("loads kinda slow") are dominated by a single edge defect with three symptoms:

  1. No HTTP compression at all. Traefik has zero compress middleware defined in either static (traefik.yml) or dynamic (config/dynamic.yml) config, and the Jellyfin router only attaches security-headers@file. Result: Jellyfin's 28 webpack JS bundles ship raw — 2.74 MiB of JS over the wire on every cold load. With gzip (default ratio ~0.30 for minified JS) that drops to ~0.82 MiB. With brotli (~0.25) it drops to ~0.69 MiB. Severity: R — fix this week.
  2. No Cache-Control on hashed-asset URLs. Every JS bundle comes back with etag + last-modified and no cache-control header. Browsers default to "heuristic freshness" (typically 10 % of last-modified age) but every reload does still issue a conditional If-None-Match request per asset and gets a 304 back. On a cold-cache page-nav that's 28 round-trips of pure negotiation overhead even when the response body is cached. These URLs are content-hashed (?7dc095d8…), so they should be Cache-Control: public, max-age=31536000, immutable. Severity: Y → R when WAN clients are involved (each round-trip costs an internet RTT).
  3. Poster image first-fetch is slow — the very first cold request for a /Items/{id}/Images/Primary triggers a Jellyfin server-side image transcode (resize + JPEG re-encode) and runs in ~385 ms wall vs ~2535 ms for warm cache. With ~20 posters on the home page and no edge cache, the first visit to "Recently Added" is a ~7-second poster grid. Doc 13 finding 23 (3 MB splash PNG) is the loud single hit; this is the death-by-a-thousand-cuts equivalent for the home page. Severity: Y.

Everything else (TLS handshake, MTU, DNS lookup, HTTP/2 vs HTTP/3, cert chain depth, Traefik middleware chain, Pi-hole hairpin) is healthy or low-impact — full table below.

Top quick win: add a compress@file middleware in /opt/docker/traefik/config/dynamic.yml and reference it from the Jellyfin router. One file edit. Two lines of YAML in the middleware block, one line on the router. ~70 % cold-load wire-size reduction.


2. Curl timing breakdown (5 samples, p50, p95)

Test: curl https://arrflix.s8n.ru/web/index.html from onyx.

LAN-direct (--resolve to 192.168.0.100)

Sample DNS CONN TLS TTFB TOTAL
1 0.000024 0.001225 0.022960 0.031569 0.040531
2 0.000024 0.001217 0.020182 0.024190 0.030353
3 0.000028 0.001437 0.025502 0.030467 0.035793
4 0.000023 0.001501 0.021998 0.037444 0.041056
5 0.000023 0.001265 0.018536 0.022942 0.027066
p50 24 µs 1.27 ms 22.0 ms 30.5 ms 35.8 ms
p95 28 µs 1.50 ms 25.5 ms 37.4 ms 41.1 ms

Hostname (onyx /etc/hosts → 192.168.0.100)

| p50 | DNS 0.34 ms | CONN 1.6 ms | TLS 23.0 ms | TTFB 27.5 ms | TOTAL 33.7 ms |

Notes

  • DNS via /etc/hosts adds ~300 µs vs --resolve. Negligible.
  • TLS handshake is the dominant cost (≥60 % of TTFB). TLS 1.3 with TLS_AES_128_GCM_SHA256, 2-cert chain depth (Let's Encrypt R13 → ISRG Root X1), no avoidable latency there. Connection reuse will hide it on subsequent requests within the same browser session.
  • TTFB ≤ 40 ms even on cold connection — server-side latency for the index.html body itself is fine. The "feels slow" perception is not in this number; it's in the 28-bundle waterfall after index.html.

3. Compression / cache header table

Probed with Accept-Encoding: gzip, br, zstd. Every asset was served raw.

Asset Type Bytes Encoding Cache-Control ETag
/web/index.html text/html 65 485 none (none) yes
/web/runtime.bundle.js?… text/js 49 152 none (none) yes
/web/main.jellyfin.bundle.js?… text/js 499 108 none (none) yes
/web/node_modules.@jellyfin.sdk.bundle.js?… text/js 740 699 none (none) yes
/web/node_modules.@mui.material.bundle.js?… text/js 381 100 none (none) yes
/web/node_modules.core-js.bundle.js?… text/js 182 469 none (none) yes
/web/node_modules.react-dom.bundle.js?… text/js 128 970 none (none) yes
/web/node_modules.@tanstack.query-core.bundle.js?… text/js 101 747 none (none) yes
/web/node_modules.lodash-es.bundle.js?… text/js 24 604 none (none) yes
/web/themes/dark/theme.css text/css 8 631 none (none) yes
/web/manifest.json json 781 none (none) yes
/web/serviceworker.js text/js 768 none (none) yes
/web/favicon.ico image/x-icon 6 830 none (none) yes
/web/touchicon.png image/png 8 515 none (none) yes
/Items/.../Images/Primary (cold) image/jpeg ~46 000 none public (no max-age)

Verification — index.html negotiated against four different Accept-Encoding headers. All four returned content-length: 65485 and no content-encoding — confirms Traefik isn't selectively disabling compression by User-Agent/path; the middleware simply isn't in the chain.

ETag-revalidation works correctly: a follow-up If-None-Match: "1db3a353daaafa4" returns HTTP/2 304 immediately — so warm-load is "fast" only because nothing has changed since cold load. The browser still pays a round-trip per asset.


4. Asset cold-load waterfall (top by size)

/web/index.html references 28 webpack-emitted JS bundles (full list at /tmp/edge-audit/bundles.txt during audit; file generated by parsing <script src=…> tags in index.html and discarded after report). All 28 share the same query-string version ?7dc095d8f634f60f309c — they ARE content-versioned URLs and SHOULD be cached immutable.

Rank Bundle Bytes Notes
1 node_modules.@jellyfin.sdk.bundle.js 740 699 Largest single file. Compresses ~70 %.
2 main.jellyfin.bundle.js 499 108 App bundle. Compresses ~70 %.
3 node_modules.@mui.material.bundle.js 381 100 MUI components. Compresses ~75 %.
4 node_modules.core-js.bundle.js 182 469 Polyfills. Compresses ~75 %.
5 node_modules.react-dom.bundle.js 128 970 React DOM. Compresses ~75 %.
6 node_modules.@tanstack.query-core.bundle.js 101 747 React-Query. Compresses ~70 %.
7 node_modules.jellyfin-apiclient.bundle.js 88 025 Compresses ~70 %.
8 node_modules.jquery.bundle.js 87 296 Compresses ~70 %.
9 node_modules.axios.bundle.js 80 291 Compresses ~70 %.
10 node_modules.date-fns.esm.bundle.js 74 309 Compresses ~70 %.
11 node_modules.@remix-run.router.bundle.js 72 992
12 37869.bundle.js 70 690 Lazy chunk.
13 runtime.bundle.js 49 152 Webpack runtime.
14 node_modules.webcomponents.js.bundle.js 39 705
15 node_modules.@mui.icons-material.bundle.js 30 861
(13 more bundles, each 530 KB) ~351 000
Total JS 28 bundles 2 806 173 (2.68 MiB raw)
+ index.html 65 485
+ theme.css + assets ~32 000
Cold-load total ~2.76 MiB uncompressed

Wall-time measurements from onyx (LAN-direct, sequential):

  • 5 top bundles, sequential GET, LAN: 0.37 s for 1.65 MiB.
  • All 28 bundles, sequential GET, LAN: 1.51 s for 2.68 MiB.

A real browser uses HTTP/2 multiplexing so won't be strictly sequential, but connection-window + flow-control mean wire-time on WAN scales nearly linearly with total bytes. Compression alone would cut wire-time ~70 %.

Estimated post-compression total: ~0.82 MiB (gzip) or ~0.69 MiB (brotli). At a 50 Mbps WAN, that's a 200300 ms cold-load saving before any RTT improvements from cache headers.


5. ServiceWorker warm-load effectiveness

Conclusion: SW does NOT cache app assets. Verified by reading /web/serviceworker.js (768 B, last modified 2024-11-19 — Jellyfin 10.10.3 ship date).

The SW only handles notificationclick events (cancel-install / restart-server actions) and a one-shot activateclients.claim(). There is no fetch handler, no install precache, no asset caching at all. This matches doc 13 finding 11.

So the warm-load is doing exactly what the browser HTTP cache + ETag flow gives us: 28 conditional GETs, each returning 304 with empty body but a full TLS-multiplexed round-trip. With proper Cache-Control: max-age=31536000, immutable on the hashed URLs, all 28 of those revalidations would collapse into zero network traffic on warm load.


6. Poster image timing

Tested against Rick and Morty series ID 548035d5e4d36cd2f488900ab612581a, /Items/{id}/Images/Primary?fillHeight=300&fillWidth=200&quality=96.

Request TTFB TOTAL Bytes
Cold (uncached size variant) 385 ms 388 ms 45 660
Warm 1 26 ms 29 ms 45 660
Warm 2 38 ms 42 ms 45 660
Warm 3 34 ms 38 ms 45 660
Warm 4 37 ms 42 ms 45 660
Cold h=400 351 ms 79 925
Cold h=500 469 ms 112 168
Cold h=600 364 ms 145 505

Response headers:

HTTP/2 200
age: 0
cache-control: public                       ← no max-age
content-disposition: attachment             ← unusual on a poster (forces 'save')
content-type: image/jpeg
last-modified: <request time>               ← unhelpful for caching
vary: Accept

Two issues here:

  • Cache-Control: public with no max-age means the browser applies heuristic freshness (10 % of last-modified age = 0 s, since last-modified equals the response time). Effectively uncached. Every navigation back to the home page re-fetches all posters.
  • Server-side image transcode is the dominant cost. Jellyfin generates the fillHeight=300&fillWidth=200&q=96 variant on demand from the source poster image, then caches it in /cache/images/. age: 0 on response confirms this was a fresh generation. Doc 13 finding 26 puts the on-disk image cache at 15 MB total — small enough that recent-cache eviction may be culling variants.

Per-poster cold cost: 350470 ms. Twenty posters at unique fillHeight × fillWidth × quality variants on the first load of "Recently Added" totals ~7 s if the browser drops to single- threaded poster fetches (HTTP/2 multiplexes, so true cost is GPU/CPU-bound on the server side). Doc 13 finding 02 (no GPU transcode, 12-core box already at load 11.4) means even this is software-rendered.

content-disposition: attachment on an image fetched into an <img> tag doesn't actually force a download (the browser ignores the disposition for media references), but it's a Jellyfin-side oddity worth noting.


7. Traefik request-log latency analysis

docker logs traefik --since 6h | grep jellyfin@docker — total 116 requests, 78 of them at 0 ms (cache hits / 304s / 401s).

Latency histogram (ms suffix on each log line):

Bucket Count
0 ms 78
1 ms 8
3 ms 1
7 ms 1
1846 ms 4
92294 ms 5
346648 ms 4
1.12.1 s 3
4.99.5 s 4

Every entry above ~50 ms is a /videos/.../hls1/main/*.mp4 HLS-segment GET, not a /web/* static asset. Decoded request URIs show the slow ones are AV1 + HEVC transcode requests with VideoBitrate=362547 Mbit and 500/499 final status — exactly the pattern doc 13 finding 03 calls out (CPU-only transcode + no throttling). Edge layer is clean: every /web/* request that appeared in the 6-hour window completed in 07 ms wall.

Status code distribution for jellyfin@docker (6 h):

Code Count
200 (filtered out by accessLog statusCodes 400-599) (not logged)
400 1
401 7
404 68
405 8
499 15
500 8
502 1

The 68 × 404 are mostly Cineplex/CSS/icon references from the bundled theme @import-ing assets that Jellyfin doesn't ship — cosmetic, but each 404 is a wasted RTT on every cold-load (browser fetches the referenced URL, gets 404, retries on next page nav). Worth a separate look in coordination with doc 09 (Cineplex theme).


8. Traefik middleware audit

Static config (/opt/docker/traefik/traefik.yml)

entryPoints:
  websecure:
    address: ":443"
    http:
      middlewares:
        - security-headers@file
        - rate-limit@file

Jellyfin router (/opt/docker/jellyfin/docker-compose.yml)

labels:
  - "traefik.http.routers.jellyfin.middlewares=security-headers@file"

Effective middleware chain at /web/* request

  1. security-headers@file (entrypoint) — header rewrites, no body processing, ~zero CPU.
  2. rate-limit@file (entrypoint) — token-bucket avg=100 burst=200 period=1s. Pure counter, ~zero CPU. Not a regex chain. Not doing CPU-significant work.
  3. security-headers@file (router, duplicate) — applied a second time to the response. Idempotent (header overwrite is a no-op when value already set), but redundant and a small CPU waste per-request. Worth deduping.

What's missing

  • compress middleware. Traefik supports it with a one-liner:
    middlewares:
      compress:
        compress: {}
    
    Defaults to gzip + brotli, sizes ≥1024 B, smart Accept-Encoding negotiation. Not present anywhere.
  • No headers.customResponseHeaders.Cache-Control override on the Jellyfin router — Traefik would let us inject Cache-Control: public, max-age=31536000, immutable for /web/*.bundle.js?* requests via a replacePath-+-headers combination, OR (cleaner) Jellyfin can be configured to send the right headers itself; this is config not architecture.

Traefik middleware chain on other Jellyfin paths

The no-guest@file allowlist seen in dynamic.yml is not referenced by the Jellyfin router (per doc 09 §1.2 it was intentionally dropped when WAN exposure was added). That matches expectation; not an edge performance issue.

The headscale-deny-leaks and signup-strict middlewares are defined but only referenced by other routers. No effect on Jellyfin.


9. DNS / hairpin / MTU

Probe Result Verdict
Pi-hole DNS lookup dig arrflix.s8n.ru @192.168.0.1 returns 82.31.156.86 (WAN) Y — split-horizon missing. Onyx's /etc/hosts pin saves it; any LAN client without that entry hairpins through the router.
Onyx hairpin to WAN IP, full TTFB 3343 ms G — hairpin works, no NAT-loopback latency penalty.
LAN MTU ping -M do -s 1472 -c 3 1480/1480/1480, 1.171.75 ms G — full 1500 MTU, no fragmentation, no PMTUD penalty.
--resolve LAN-direct vs hostname DNS adds 300 µs G — negligible.

The Pi-hole gap is a doc-09-related exposure decision: arrflix.s8n.ru has public DNS on Gandi pointing at the WAN IP, no Pi-hole local override. For an LAN-first deploy you'd add a local DNS rewrite arrflix.s8n.ru → 192.168.0.100 on Pi-hole. Per memory note feedback_s8n_hosts_override.md, this is a known pattern (/etc/hosts pin on each device works, but doesn't scale to phones).


10. HTTP/2, HTTP/3, TLS

  • HTTP/2: confirmed (HTTP/2 200 response, multiplexing available).
  • HTTP/3 (QUIC): not enabled. Alt-Svc header is absent on every probe. (My local libcurl doesn't support --http3 so I can't client-test, but the lack of Alt-Svc advertises that the server doesn't speak QUIC.) Traefik ≥ 2.8 supports HTTP/3 via experimental entryPoints.websecure.http3 = {} block; not enabled in traefik.yml. Y — would help WAN clients on lossy links (mobile data, café WiFi); near-zero benefit on LAN.
  • TLS chain: 2 certs (leaf + LE R13 intermediate) → ISRG Root X1 is in client trust store. Chain length is minimal; not contributing to handshake latency.
  • TLS version: 1.3 with AEAD cipher (TLS_AES_128_GCM_SHA256).
  • sniStrict: true in dynamic.yml's tls.options.default. Correct.

11. Concrete remediation list (ranked by impact-per-effort)

# Fix Effort Impact Risk
1 Add compress@file middleware in /opt/docker/traefik/config/dynamic.yml: compress: {} under http.middlewares.compress. Reference it from the Jellyfin router via a traefik.http.routers.jellyfin.middlewares=security-headers@file,compress@file label edit in /opt/docker/jellyfin/docker-compose.yml. S (5 min) ~70 % cold-load wire reduction (2.74 MiB → ~0.82 MiB). Lowers TTI on every single first-visit. Low — Traefik's compress is a standard middleware, gzip+br, content-type allow-list does the right thing for application/javascript + text/html + text/css. Will not compress image/jpeg.
2 Add Cache-Control: public, max-age=31536000, immutable for /web/*.bundle.js?* and /web/*.css?* requests. Cleanest path is via Traefik headers middleware with customResponseHeaders.Cache-Control and a router rule that matches Path(\/web/`) && Query(`hash`)` — but Jellyfin can also be patched at the source if there's appetite. SM Eliminates 28 × per-page-nav round-trips for warm load. Saves ~28 RTTs (~1.5 s on a 50-ms WAN link, ~0 on LAN). Medium — must scope ONLY to hashed URLs; if Cache-Control: immutable is applied to index.html you brick the next deploy until users force-reload.
3 Enable HTTP/3 / QUIC. Add entryPoints.websecure.http3 = {} to traefik.yml, expose UDP 443 on the host, and add an Alt-Svc: h3=":443"; ma=86400 header (Traefik does this automatically once the HTTP/3 entrypoint is on). M Marginal on LAN, real on lossy WAN (3G, café WiFi). Cuts TLS handshake to 1-RTT. Low — Traefik HTTP/3 has been stable since v3.0; coexists with H/2. Need to open UDP 443 on nullstone firewall + router port-forward.
4 Tighten poster image cache. Either set Cache-Control: public, max-age=86400 on /Items/*/Images/Primary responses (Jellyfin-side via system.xml MaxResumePct style — actually a Jellyfin web-server-config patch), or put a Traefik-level headers.customResponseHeaders.Cache-Control on Path(\/Items/`) && PathPrefix(`/Images/`)`. Even 1 hour of caching collapses the poster grid re-fetch on home-page bounce-back. SM ~7 s saved on home-page revisit when posters were already fetched. Low — posters are content-addressed by ?fillHeight=…&quality=…; safe to cache.
5 Dedupe security-headers middleware. Remove the entrypoint-level security-headers@file OR remove it from each per-router label. (Cleanest: keep it at entrypoint level, drop from labels.) S Tiny (microseconds per request). Cleanup, not perf. Low.
6 Add Pi-hole local DNS rewrite for arrflix.s8n.ru192.168.0.100. Memory note feedback_s8n_hosts_override.md already covers this pattern. Onyx /etc/hosts works but doesn't scale to phones / friends' devices. S Stops LAN clients hairpinning through router on every fetch. Saves 1× NAT-loopback round-trip per TCP connection (~2 ms — small but free). Low.
7 Investigate the 68 × 404 in 6 h on /web/*. Likely Cineplex theme @import or icon references with bad paths. Each 404 is a wasted RTT on cold-load. S Small but cumulative on cold-load. Low — read-only investigation first.
8 Strip content-disposition: attachment on Image responses. Jellyfin emits this on every /Images/Primary GET. Browser ignores it for <img> references but it's hostile if anyone right-clicks "open image in new tab". S Cosmetic. Low.

The order #1 → #2 → #3 is the entire cold-load story. #1 alone turns "kinda slow" into "fine" for 90 % of the perceived latency on first load. #2 turns 2nd-page-nav into "instant" by eliminating the 28-asset revalidation tax. #3 is the WAN-optimist nice-to-have; do once mobile clients matter.

Out of scope for this audit but worth noting from doc 13: GPU transcode re-enable (#02 there) is the real win for playback latency. Cold-load + playback are separate paths; both need attention.


12. Out of scope (audited and found healthy)

  • TLS handshake latency (2225 ms LAN, normal for TLS 1.3 fresh handshake; reuse hides it).
  • Cert chain depth (2-cert chain, R13 intermediate).
  • MTU (1500, no fragmentation).
  • HTTP/2 (working, multiplexed).
  • DNS lookup (300 µs via /etc/hosts; 20160 ms first time via Pi-hole, cached after).
  • Hairpin NAT (works, no extra latency).
  • rate-limit@file middleware (token-bucket, ~zero overhead).
  • Sniff/CSP/STS/frame headers — set correctly, no perf cost.
  • ServiceWorker (notification-only, no perf-positive nor perf-negative).
  • Traefik access log filter (statusCodes 400-599 only — does NOT log the 200 OK responses that dominate /web/*; the latency histogram in §7 is therefore a 5xx/4xx-only sample, not full traffic. The 5xx/4xx sample is conclusive enough for edge analysis because all the slow ones are HLS transcode failures, not edge problems).

Appendix — raw evidence

Curl timing (LAN-direct, 5 samples)

DNS=0.000024 CONN=0.001225 TLS=0.022960 TTFB=0.031569 TOTAL=0.040531
DNS=0.000024 CONN=0.001217 TLS=0.020182 TTFB=0.024190 TOTAL=0.030353
DNS=0.000028 CONN=0.001437 TLS=0.025502 TTFB=0.030467 TOTAL=0.035793
DNS=0.000023 CONN=0.001501 TLS=0.021998 TTFB=0.037444 TOTAL=0.041056
DNS=0.000023 CONN=0.001265 TLS=0.018536 TTFB=0.022942 TOTAL=0.027066

Compression negotiation matrix

Accept-Encoding: br        → content-length: 65485, no content-encoding
Accept-Encoding: gzip      → content-length: 65485, no content-encoding
Accept-Encoding: (empty)   → content-length: 65485, no content-encoding
Accept-Encoding: gzip,deflate,br,zstd --compressed → content-length: 65485, no content-encoding

TLS chain

depth=2 C=US, O=Internet Security Research Group, CN=ISRG Root X1
depth=1 C=US, O=Let's Encrypt, CN=R13
depth=0 CN=arrflix.s8n.ru
Verification: OK
Protocol: TLSv1.3
Cipher:   TLS_AES_128_GCM_SHA256

ETag-conditional revalidation

First fetch:  HTTP/2 200, etag "1db3a353daaafa4", content-length 499108
If-None-Match: HTTP/2 304, etag "1db3a353daaafa4", body empty

Bundle inventory (28 bundles, total 2 806 173 bytes)

Top 15 by size — see §4 table. Full list reproducible from curl -s https://arrflix.s8n.ru/web/index.html | grep -oE 'src="[^"]*\.bundle\.js[^"]*"'.

Poster image fetch (5 samples — first cold, rest warm)

TTFB=0.385230s TOTAL=0.388290s SIZE=45660b   ← cold (server transcode)
TTFB=0.025961s TOTAL=0.028951s SIZE=45660b
TTFB=0.037838s TOTAL=0.041724s SIZE=45660b
TTFB=0.034244s TOTAL=0.038364s SIZE=45660b
TTFB=0.036687s TOTAL=0.041616s SIZE=45660b

Traefik static config (entrypoints)

websecure:
  address: ":443"
  http:
    middlewares:
      - security-headers@file
      - rate-limit@file

Jellyfin router labels (compose)

"traefik.http.routers.jellyfin.middlewares=security-headers@file"
"traefik.http.services.jellyfin.loadbalancer.server.port=8096"

MTU + ping

PING 192.168.0.100 (192.168.0.100) 1472(1500) bytes of data
1480 bytes from 192.168.0.100: icmp_seq=1 ttl=64 time=1.66 ms
1480 bytes from 192.168.0.100: icmp_seq=2 ttl=64 time=1.75 ms
1480 bytes from 192.168.0.100: icmp_seq=3 ttl=64 time=1.17 ms
0 % packet loss, rtt min/avg/max/mdev = 1.171/1.524/1.745/0.252 ms

Pi-hole DNS resolution

$ dig +short arrflix.s8n.ru @192.168.0.1
82.31.156.86           ← public WAN IP, not the LAN 192.168.0.100

Traefik request-log latency histogram (jellyfin@docker, 6 h, 5xx/4xx only — 200s filtered out)

   78 0ms
    8 1ms
    1 3ms
    1 7ms
    1 18ms
    1 29ms
    1 39ms
    1 46ms
    1 92ms
    1 175ms
    1 192ms
    1 209ms
    1 222ms
    1 274ms
    1 294ms
    1 346ms
    1 391ms
    1 648ms
    1 1168ms
    1 1256ms
    1 2140ms
    1 4931ms
    1 8118ms
    1 9543ms

All entries >50 ms are /videos/.../hls1/main/*.mp4 — HLS transcode requests with 500/499 status, AV1+HEVC at 360550 Mbit source. Edge is not the bottleneck on those; CPU transcode is (doc 13 #02, #03).


Sign-off

  • Audit: 2026-05-08, read-only, ~30 min wall.
  • No fixes applied. No state mutated. No container restart. No Traefik reload. No header injected. Admin token used only for read-side /Items and /Items/.../Images probes.
  • Next audit due: after fix #1 ships, to confirm gzip/brotli ratio on the actual deployed config and re-measure cold-load.