From 0b14e394adb4f8ba2f2adbc688547ee3dd232610 Mon Sep 17 00:00:00 2001 From: grabowski Date: Wed, 12 Aug 2026 11:51:43 +0700 Subject: [PATCH] perf: Cache-Control headers so browsers and edge caches absorb traffic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bandwidth diagnosis: iperf shows the proxy<->API VPN link at 65-78 Mbit/s and a fast client pulls 15.7 MB/s from a CDN but only ~66 KB/s from the site — the Caddy host's public uplink is the scarce resource. Max-ages mirror the server cache TTLs (latest 30s, HII 60s, forecast 120s, history/stats/stations 300s, static 1h, dashboard HTML 120s), so repeat views come from browser cache and an edge proxy (e.g. Cloudflare free tier in front of Caddy) can serve most traffic without touching the uplink. --- src/web_api.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/web_api.py b/src/web_api.py index 4b06ec5..03ac09a 100644 --- a/src/web_api.py +++ b/src/web_api.py @@ -348,6 +348,35 @@ def _send_umami_event(path: str, method: str, status: int, host: str, user_agent pass # analytics must never affect API behavior +# Cache-Control by path: lets browsers and any edge cache (e.g. Cloudflare in +# front of Caddy) absorb repeat traffic — the public uplink is the scarce +# resource. Max-ages mirror the server-side cache TTLs / data cadence. +_CACHE_CONTROL_RULES = ( + ("/static/", "public, max-age=3600"), + ("/measurements/latest", "public, max-age=30"), + ("/api/hii/", "public, max-age=60"), + ("/measurements/history", "public, max-age=300"), + ("/forecast", "public, max-age=120"), + ("/api/stats", "public, max-age=300"), + ("/stations", "public, max-age=300"), +) + + +@app.middleware("http") +async def cache_control_headers(request, call_next): + response = await call_next(request) + if request.method == "GET" and response.status_code == 200: + path = request.url.path + if path == "/": + response.headers.setdefault("Cache-Control", "public, max-age=120") + else: + for prefix, value in _CACHE_CONTROL_RULES: + if path.startswith(prefix): + response.headers.setdefault("Cache-Control", value) + break + return response + + @app.middleware("http") async def umami_api_tracking(request, call_next): response = await call_next(request)