Cache jen jako fallback vyčerpá denní limit externího API
import { Aside } from ‘@astrojs/starlight/components’;
Symptom
Sekce “Symptom”Integrace s externím API (předpověď počasí) přestala odpoledne fungovat: část stránek hlásila v konzoli 500 Internal Server Error na endpointu, který tahá předpověď. Jiné lokality fungovaly. V serverovém logu se opakovaně objevovalo „API selhalo, vracím poslední známé z cache”. Přímé volání upstreamu vrátilo:
{"error":true,"reason":"Daily API request limit exceeded. Please try again tomorrow."}HTTP 429. Denní limit free tieru (10 000 req/den) byl vyčerpán už během dne.
Root cause
Sekce “Root cause”Service měl cache, ale používal ji jen jako fallback při selhání, ne jako primární zdroj:
async function getForecast(lat, lng, days) { try { const res = await fetch(UPSTREAM_URL); // ← VŽDY živé volání const data = await res.json(); await cache.upsert({ key, value: { ts: Date.now(), data } }); // plní cache return data; } catch (e) { const cached = await getCached(lat, lng, days); // ← cache se čte JEN tady if (cached?.length) return cached; throw e; // ← bez cache → 500 }}Dopady:
- Žádná úspora požadavků. Cache se plnila, ale nikdy nečetla na happy-path → každé načtení dashboardu/kanbanu, každý cron a každé otevření detailu volalo upstream živě. Při desítkách entit × více načtení za den + cronech to přeteklo denní limit.
- Po vyčerpání limitu lavina 429. Jakmile upstream začal vracet 429, fungovaly jen lokality s už naplněnou cache. Lokality bez cache spadly do
throw→ route vrátila 500 (ne graceful prázdno).
Fix
Sekce “Fix”Dvě změny ve službě:
- Primární TTL cache — čerstvý záznam (< TTL) se vrátí rovnou bez volání upstreamu:
const TTL_MS = 3 * 60 * 60 * 1000; // data se nemění po minutách
const fresh = await cache.findUnique({ where: { key } });if (fresh?.value) { const { ts, data } = JSON.parse(fresh.value); if (ts && Date.now() - ts < TTL_MS && data?.length) return data; // ← bez volání API}// ... jinak teprve živé volání + naplnění cache- Graceful degradace místo 500 — když upstream selže A není cache, vrať prázdno (200), ať se UI jen nezobrazí featura, ale zbytek funguje:
} catch (e) { const cached = await getCached(...); if (cached?.length) return cached; console.warn("upstream selhalo a žádná cache:", e.message); return []; // ← místo `throw e`}Po nasazení: objem volání upstreamu spadl řádově (z „každý request” na „1× za TTL na lokalitu”) → drží pod denním limitem; a 500 zmizely (prázdná předpověď = 200).
Jak se tomu vyvarovat v jiných systémech
Sekce “Jak se tomu vyvarovat v jiných systémech”- Detection: v service grepni cache zápis (
cache.upsert/set) a podívej se, jestli existuje i čtení na začátku funkce, ne jen vcatch. Pokud se čte jen vecatch→ cache je jen fallback, ne TTL cache. - Anti-pattern: „cache plním vždy, čtu jen při chybě.” Vypadá to jako caching, ale upstream je pořád zatížený 1:1.
- Lepší přístup: read-through cache s TTL: nejdřív zkus čerstvou cache → teprve při miss volej upstream → naplň. A externí integrace nikdy nenech spadnout na 500 jen kvůli nedostupnosti — degraduj na prázdný/poslední-známý výsledek.
- Bonus: sleduj
429/rate-limit hlavičky upstreamu a měj alert dřív, než limit přeteče.
Sister bugs / související
Sekce “Sister bugs / související”Příbuzné: jakákoliv integrace, kde se „cache” reálně používá jen jako záchrana při výpadku (geokódování, kurzy měn, dlaždice map). Symptom bývá stejný: funguje to, dokud nepřeteče limit nebo upstream nespadne, a pak tvrdá chyba místo degradace.