Az edge azért éri meg, mert a válasz a felhasználó mellett születik — de ez csak akkor igaz, ha jól cache-elsz. Ez a modul a három cache-réteget, a multitenant cache-kulcs kérdését, a purge-stratégiát és a latencia-optimalizálás eszközeit veszi végig.
Ez a leggyakoribb zavarforrás, ezért kezdjük ezzel. A Workers körül három különböző cache-mechanizmus van, más-más viselkedéssel:
| Workers Caching | Cache API (caches.default) | fetch() + cf objektum | |
|---|---|---|---|
| Mi ez | a Worker elé tett read-through cache | alacsonyszintű, programozott cache-tár | a kimenő subrequestek cache-elése |
| Lefut-e a Worker cache-találatnál? | nem — ez a lényeg | igen, mindig | — |
| Kérés-összevonás (thundering herd ellen) | van | nincs | van |
| Tiered cache | van | nincs (adatközpont-lokális) | van |
| Purge | ctx.cache.purge() | cache.delete() — csak lokálisan; globálishoz zóna-purge/tag | zóna-purge, tag |
| Mikor használd | új projektben ez az alapértelmezett | ha finomhangolt, programozott kontroll kell | ha origin/külső API válaszát cache-eled |
{
"cache": { "enabled": true }
}
Innentől a Worker válaszainak cache-elhetőségét a szokásos Cache-Control header vezérli — vagyis ugyanaz a mechanizmus, amit HTTP-ből ismersz, csak most a saját válaszaidra alkalmazod:
return new Response(body, {
headers: {
// 5 percig friss; utána 1 óráig kiadható a régi, míg a háttérben frissül
"Cache-Control": "public, max-age=300, stale-while-revalidate=3600",
},
});
Multitenant SaaS-ban ugyanaz az URL (/api/dashboard) tenantonként és felhasználónként más tartalmat ad. Ha a cache-kulcs csak az URL, akkor A tenant válasza kimehet B tenantnak — ez a legdurvább hibaosztály, ami cache-eléssel elkövethető.
Négy megközelítés, növekvő biztonsággal:
| Módszer | Hogyan | Értékelés |
|---|---|---|
| Ne cache-eld | Cache-Control: private, no-store minden bejelentkezett válaszra | a legbiztonságosabb alap — ezzel kezdj |
| Tenant az URL-ben | acme.app.com/api/dashboard vagy /t/acme/api/dashboard | egyszerű és robusztus: a kulcs természetesen elválik |
| Egyedi cache-kulcs | a kulcsba beleveszed a bérlőt | működik, de fegyelmet igényel — egy kifelejtett hely elég a bajhoz |
ctx.props alapú izoláció | service-binding hívásoknál a hívó azonossága automatikusan a kulcs része | a legerősebb: nem lehet „elfelejteni", nem kerülhető meg |
private, no-store), és csak tudatosan, esetenként engedd meg — soha ne fordítva. Ha bizonytalan vagy egy végpontnál, az azt jelenti, hogy nem cache-elhető. A cache-elésből nyert néhány ezredmásodperc soha nem éri meg egy tenant-adatszivárgás kockázatát.| Tartalom | Réteg | Stratégia |
|---|---|---|
statikus assetek (/_nuxt/*, képek) | Static Assets (3. modul) | automatikus, fingerprintelt fájlnév → max-age=31536000, immutable |
| publikus marketing-oldalak, blog | Workers Caching / routeRules | swr vagy isr — percek-órák |
| publikus API (árlista, feature-lista) | Workers Caching | public, max-age=60, stale-while-revalidate |
| bejelentkezett dashboard HTML | — | ne cache-eld; helyette gyors SSR + streaming (18.5) |
| drága, ritkán változó számítás (riport, aggregátum) | KV vagy D1, nem HTTP-cache | alkalmazás-szintű cache tenant-kulccsal (6. modul) |
| külső API válasza (árfolyam, geo-adat) | fetch() + cf.cacheTtl vagy KV | a partner rate limitjét is védi |
| session/jogosultság | cookie vagy KV (17. modul) | soha ne HTTP-cache-be |
Az alkalmazás-szintű cache (KV-ben) az, amit a leggyakrabban fogsz használni, mert a legtöbb SaaS-tartalom nem HTTP-cache-elhető, viszont a mögötte lévő számítás igen:
server/utils/cached.tsexport async function cached<T>(
event, key: string, ttl: number, produce: () => Promise<T>
): Promise<T> {
const { env } = event.context.cloudflare;
const tenantId = event.context.tenantId; // 17. modul middleware-ből
const ns = `v1:${tenantId}:${key}`; // verzió + bérlő a kulcsban!
const hit = await env.CACHE.get(ns, { type: "json" });
if (hit) return hit as T;
const fresh = await produce();
// nem várunk rá: a válasz mehet, az írás a háttérben fut (2. modul)
event.context.cloudflare.context.waitUntil(
env.CACHE.put(ns, JSON.stringify(fresh), { expirationTtl: ttl })
);
return fresh;
}
Két részlet, ami sokat számít: a kulcsban ott a verzió-prefix (v1:) — ha a válasz szerkezete változik, a prefix emelésével az egész cache-t érvényteleníted deploy nélkül; és a bérlő — lásd 18.2.
„A számítástechnika két nehéz problémája: a cache-invalidálás és az elnevezés." A gyakorlati eszköztárad:
| Eszköz | Hatókör | Mikor |
|---|---|---|
| TTL lejárat | automatikus | az esetek 90%-a — ha elviselhető a késés, ez a legegyszerűbb |
| stale-while-revalidate | automatikus | ha a frissítés lassú, de a régi adat elfogadható közben |
| Verzió a kulcsban | minden érintett bejegyzés | séma- vagy formátumváltásnál (deploy nélküli globális invalidálás) |
| Cache-Tag + purge by tag | címkézett bejegyzések | „az acme tenant minden cache-elt oldala" — a válaszra Cache-Tag-et teszel, majd tag szerint purge-elsz |
ctx.cache.purge() | Workers Caching bejegyzés | célzott invalidálás írás után |
| Purge everything / prefix / host | zóna-szint | vészhelyzet vagy nagy release |
cache.delete() csak abban az adatközpontban törli, ahol a Worker éppen fut — globális törléshez zóna-szintű purge (tag, prefix, host, everything) kell. ② Egyedi cache-kulccsal cache-elt URL-t nem tudsz URL szerint purge-elni — ilyenkor tag vagy prefix szerinti purge az út. Ezért érdemes a purge-stratégiát a cache-kulcs megtervezésekor eldönteni, nem utólag.// riport frissítése után: a tenant összes riport-cache-ét eldobjuk
export default defineEventHandler(async (event) => {
await updateReport(event, data);
const { env } = event.context.cloudflare;
const tenantId = event.context.tenantId;
// KV: a verziószámot emeljük tenantonként — az összes régi kulcs elárvul
await env.CACHE.put(`ver:${tenantId}`, String(Date.now()));
return { ok: true };
});
A „verzió-token" minta gyakran jobb, mint a törölgetés: nem kell tudnod, milyen kulcsok léteznek — egy szám emelésével az összes régi bejegyzés érvénytelenné válik, és a TTL magától kitakarítja őket.
Az 5b. modulban már felmerült: ha egy kérés sok lekérdezést lő ugyanarra az adatbázisra, a Worker futtatása a felhasználó mellett rosszabb, mint az adatbázis mellett — mert minden query átmegy az óceánon. A Smart Placement ilyenkor magát a Workert helyezi át:
{ "placement": { "mode": "smart" } }
Mikor segít: több (3-4+) egymás utáni DB-hívás kérésenként. Mikor nem: ha kérésenként egy lekérdezés van — akkor a teljes körút ugyanannyi, akárhol fut a Worker (a doksi ezt külön kiemeli). Mérd meg, ne tippelj.
// ❌ 3 × 80 ms = 240 ms
const a = await q1(); const b = await q2(); const c = await q3();
// ✅ ~80 ms
const [a, b, c] = await Promise.all([q1(), q2(), q3()]);
(Vigyázz a 6 egyidejű kimenő kapcsolat korlátjára — 2. modul.)batch() (5. modul), Postgresnél kevesebb, jobban megírt query — a Hyperdrive cache is itt segít (4. modul).Workers Caching read-through: cache-találatnál a Workered meg sem hívódik (nincs CPU-költség), összevonja az egyidejű azonos kéréseket, és részt vesz a tiered cache-ben. A Cache API alacsonyszintű tár: a Worker mindig lefut, nincs kérés-összevonás, és adatközpont-lokális.
Alapértelmezésben ne legyen cache-elhető (private, no-store). Ha mégis kell: a bérlő legyen a kulcs része — legrobusztusabban úgy, hogy az URL-ben van (aldomain vagy path), vagy service-binding esetén a ctx.props automatikus izolációjával.
cache.delete() globális invalidáláshoz?
Mert csak abban az adatközpontban töröl, ahol a Worker éppen fut. Globálishoz zóna-szintű purge kell: tag, prefix, host vagy purge everything — és egyedi cache-kulcsú bejegyzést URL szerint egyáltalán nem lehet purge-elni.
Akkor segít, ha egy kérés több egymás utáni hívást intéz ugyanahhoz a távoli adatbázishoz — ilyenkor a Workert érdemes a DB mellé tenni. Nem segít, ha kérésenként egyetlen lekérdezés van: a teljes körút ugyanannyi, bárhol is fut a Worker.
A cache-kulcs tartalmaz egy verziószámot (globálisan vagy tenantonként). Invalidáláskor nem kulcsokat törölsz — amiket nem is feltétlen ismersz —, hanem emeled a verziót: az összes régi bejegyzés elárvul, és a TTL kitakarítja őket.