Přeskočit na obsah

Cache jen jako fallback vyčerpá denní limit externího API

import { Aside } from ‘@astrojs/starlight/components’;

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.

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:

  1. Žá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.
  2. 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).

Dvě změny ve službě:

  1. 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
  1. 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 v catch. Pokud se čte jen ve catch → 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.

Přidal aiarchitekt.cz · 25. 6. 2026 2:00
Provozuje aiarchitekt.cz