Cloudflare Workers Cache

Edge cache před vaším Workerem - zapnutí, pravidla cache, ochrana proti otrávení a purge

Praktický návod pro Next.js na OpenNext i pro jakýkoli jiný Worker: jak funguje cache před Workerem, kolik stojí, jaké hlavičky posílat, jak udržet HTML a RSC od sebe a jak mazat cache bez deploye. Každý příkaz a soubor zkopírujete jedním kliknutím.

Vladimír NeporAutorIng. Vladimír Nepor
Publikováno
Jak funguje Workers Cache: požadavek nejdřív dorazí do cache před Workerem. HIT se vrátí zhruba za 50 ms a Worker vůbec neběží, jen MISS spustí worker-entry.mjs a render Next.js.PožadavekGET /blog/postGET /…?_rsc=x1y2zklíč = cesta + queryWorkers Cachenižší vrstva · colovyšší vrstva · síťHIT ≈ 50 msWorker vůbec neběžíworker-entry.mjsRender Next.jsMISS ≈ 0,4 s+ hlavičky cache02 · ZAPNUTÍZapnutí Workers Cachewrangler.jsonc + worker-entry.mjskapitola →03 · PRAVIDLA CACHEEdge × prohlížeč7 dní na edge, 10 min v prohlížečikapitola →04 · KLÍČ CACHEOchrana proti otráveníHTML a RSC se nesmí smíchatkapitola →05 · PURGEPurge bez deployectx.cache.purge() za tokenemkapitola →
Obsah návodu

00 · Přehled

Workers Cache v kostce

Workers Cache je cache, která stojí před vaším Cloudflare Workerem. Při zásahu (HIT) Cloudflare vrátí uloženou odpověď a váš kód se vůbec nespustí: žádný čas CPU, žádný studený start, žádná čtení z databáze. Zapíná se jedním řádkem ve wrangler.jsonc a co se uloží a na jak dlouho, rozhodují hlavičky odpovědi, které Worker posílá.

Celé nastavení ve zkratce

  1. Zapnutí - "cache": { "enabled": true } a "workers_dev": false ve wrangler.jsonc
  2. Pravidla v hlavičkách - malý worker-entry.mjs označí vyrenderované stránky jako cachovatelné a vše ostatní jako no-store
  3. Ochrana klíče cache - uložit jen odpověď, jakou by pod danou URL čekal běžný návštěvník (nikdy RSC payload pod adresou stránky)
  4. Purge a ověření - endpoint POST /__cache-purge chráněný tokenem a pár kontrol hlavičky Cf-Cache-Status přes curl

Co potřebujete vyřešit?

OtázkaKde najdete odpověď
Vyplatí se Workers Cache pro můj projekt? Kolik stojí?Jak to funguje →
Chci ji zapnout pro Worker s Next.js (OpenNext).Zapnutí →
Jak dlouho mají stránky zůstat na edge a v prohlížeči?Pravidla cache →
Návštěvníci místo stránky dostali surová RSC data.Klíč cache a otrávení →
Změnil jsem obsah a web pořád ukazuje starou verzi.Purge →
Co se stane s cache po deployi nebo rollbacku?Deploy a rollback →
Jak ověřím, že to funguje?Ověření →
Dnes cachujeme přes caches.default ve Workeru.Přechod z Cache API →
Čeho se návod týká

Workers Cache, jak ji Cloudflare spustil v červenci 2026, Wrangler 4.69 nebo novější, stav k říjnu 2026. Příklady jsou pro Next.js na OpenNext, protože tam jsou největší pasti (RSC payloady, s-maxage). U jiného Workeru část o RSC přeskočte, zbytek platí beze změny. Nastavení běží v produkci na našich vlastních webech.

01 · Základy

Jak Workers Cache funguje a kolik stojí

Bez Workers Cache spustí každý request váš Worker. I vlastní cache přes Cache API (caches.default) pomůže až poté, co se Worker spustí. Workers Cache odpovídá dřív než Worker, z cache sdílené celou sítí Cloudflare.

Dvě vrstvy a slučování požadavků

  • Nižší vrstva v datacentru (colo) blízko návštěvníka, vyšší vrstva sdílená celou sítí. První render kdekoli na světě naplní vyšší vrstvu, takže ostatní datacentra dostanou HIT bez dalšího renderu.
  • Slučování požadavků. Když na stejnou necachovanou URL přijde naráz mnoho požadavků, Cloudflare spustí Worker jednou za datacentrum a výsledek vrátí všem.
  • stale-while-revalidate. Po vypršení dostane další návštěvník uloženou kopii okamžitě a Worker na pozadí vyrenderuje novou. Nikdo nečeká na render.
  • Naměřeno na produkčním webu s Next.js: HIT kolem 0,05 s, MISS kolem 0,4 s.

Za co se platí

PožadavekÚčtuje sePoznámka
HITjen requestWorker neběží, žádný čas CPU
MISS, BYPASSrequest + CPUstejně jako bez cache
Statické soubory (JS, CSS, obrázky)requestjinak zdarma; se zapnutou cache se účtuje každý request na Worker, včetně /_next/static
Volání mezi Workeryrequestúčtují se i service bindings a ctx.exports
Purge, úložištěnic navícWorkers Cache nemá samostatnou cenu
Pozor na requesty statických souborů

Stránka, která ze stejného Workeru načte 20 souborů, znamená 21 účtovaných requestů (na placeném plánu $0,30 za milion nad limit plánu, na Free plánu se počítají do denního limitu). Úspora CPU za ušetřené rendery to obvykle mnohonásobně vyváží, ale první týdny kontrolujte billing. Velká média a soubory ke stažení je lepší servírovat z veřejného R2 bucketu nebo ze samostatného Workeru bez cache.

Přínosy a nevýhody

Přínosy

  • Vyšší hit rate po celém světě: jeden render naplní cache pro všechna datacentra.
  • HIT nespouští žádný kód: žádné CPU, žádný studený start, méně čtení z D1 a R2.
  • stale-while-revalidate a stale-if-error: po vypršení se nečeká a při chybě renderu se vrátí uložená kopie.
  • Purge podle tagu nebo cesty přímo z Workeru, bez deploye.
  • Méně vlastního kódu než ruční vrstva nad Cache API.

Nevýhody

  • Requesty statických souborů se účtují.
  • Klíč cache neobsahuje host, cookies ani hlavičky požadavku. Ochranu si musíte udělat sami.
  • Při HIT nevznikají logy Workeru. Stav cache ukazuje jen hlavička Cf-Cache-Status a Workers Observability.
  • Nejde předehřát: každý deploy začíná s prázdnou cache.
  • OpenNext zatím nemá oficiální integraci, hlavičky nastavuje malý wrapper.

Kdy se nehodí

  • Stránky se liší podle přihlášeného uživatele a nejde je oddělit cestou (cookies nejsou součástí klíče).
  • Většinu provozu tvoří statické soubory ze stejného Workeru a blížíte se limitu requestů na Free plánu.
  • Obsah se musí změnit během sekund a po každé změně nemáte jak zavolat purge.

02 · Zapnutí

Zapnutí Workers Cache krok za krokem

Pět kroků pro existující aplikaci Next.js na OpenNext. Wrapper worker-entry.mjs stojí mezi Cloudflare a Workerem OpenNext. Mění jen hlavičky odpovědi, takže na tom, jak aplikace renderuje, nic nemění.

Krok 1: Aktualizujte Wrangler

Workers Cache vyžaduje Wrangler 4.69.0 nebo novější (4.107.0 pro nastavení po entrypointech a cross_version_cache).

Terminál
npm install -D wrangler@latest
npx wrangler --version   # 4.69.0 or newer

Krok 2: Zapněte cache ve wrangler.jsonc

Nasměrujte main na wrapper a zapněte cache. Zbytek konfigurace (bindings, D1, R2) nechte, jak je.

wrangler.jsonc
{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "my-app",
  // instead of ".open-next/worker.js": the wrapper sets the cache headers
  "main": "./worker-entry.mjs",
  "compatibility_date": "2026-07-01",
  "compatibility_flags": ["nodejs_compat"],
  "assets": { "directory": ".open-next/assets", "binding": "ASSETS" },

  // Workers Cache: Cloudflare answers from the cache before the worker runs
  "cache": { "enabled": true },

  // The cache key has no host: *.workers.dev would share (and fill) the same cache
  "workers_dev": false
}

workers_dev: false je důležité: klíč cache neobsahuje host, takže adresa *.workers.dev by servírovala a plnila stejnou cache a přesměrování uvnitř Workeru by se při HIT nikdy nespustilo.

Krok 3: Přidejte worker-entry.mjs

Vytvořte soubor v kořeni projektu a do SITE_HOST dejte produkční host. Jednotlivé části vysvětlují kapitoly Pravidla cache a Klíč cache.

worker-entry.mjs
// worker-entry.mjs - cache policy for Workers Cache in front of an OpenNext (Next.js) worker.
// Workers Cache answers BEFORE this code runs. This file only decides, through response
// headers, what the edge may store. Anything not explicitly allowed gets no-store.
import openNextWorker from "./.open-next/worker.js";
// export { DOQueueHandler } from "./.open-next/worker.js"; // only if you use the DO queue

const SITE_HOST = "www.example.com"; // the only host whose pages may be stored
const NEVER_CACHE = ["/api/"];        // path prefixes that are never stored

// Edge: 7 days fresh, then 1 more day stale-while-revalidate / stale-if-error.
// Use max-age, not s-maxage: s-maxage and must-revalidate switch stale-while-revalidate off.
const EDGE_PAGE_CC = "max-age=604800, stale-while-revalidate=86400, stale-if-error=86400";
// Browser: 10 minutes. The edge copy can be purged at any time, the browser copy cannot.
const CLIENT_PAGE_CC = "public, max-age=600";
const NO_STORE = "no-store";

// Edge-only header: highest precedence, stripped before the response reaches the browser.
const EDGE_CC = "Cloudflare-CDN-Cache-Control";

// Headers the Next.js router sends with RSC requests (client-side navigation, prefetch).
const ROUTER_HEADERS = ["rsc", "next-router-state-tree", "next-router-prefetch", "next-router-segment-prefetch", "next-url"];

/* ---------- canonical variant guard ----------
 * The cache key is path + query only. If an RSC payload is ever stored under a page URL,
 * every visitor gets raw RSC instead of HTML until the entry expires. So only the response
 * a normal visitor expects under that URL may be stored:
 *   HTML: no _rsc parameter and no router headers,
 *   RSC:  "RSC: 1" and _rsc equal to the hash Next.js derives from the router headers
 *         (next/dist/shared/lib/router/utils/cache-busting-search-param.js).
 * If Next.js ever changes the formula, RSC simply stops being cached (fail-safe).
 */
function djb2(str) {
  let hash = 5381;
  for (let i = 0; i < str.length; i++) hash = ((hash << 5) + hash + str.charCodeAt(i)) & 0xffffffff;
  return hash >>> 0;
}

function expectedRscParam(req) {
  const h = req.headers;
  const p = h.get("next-router-prefetch");
  const prefetch = p === "1" || p === "2" ? p : undefined;
  const segment = h.get("next-router-segment-prefetch") || undefined;
  const tree = h.get("next-router-state-tree") ?? undefined;
  const nextUrl = h.get("next-url") ?? undefined;
  if ([prefetch, segment, tree, nextUrl].every((v) => v === undefined)) return "";
  return djb2([prefetch || "0", segment || "0", tree || "0", nextUrl || "0"].join(",")).toString(36).slice(0, 5);
}

function isCanonicalPage(req, url, type) {
  const rsc = url.searchParams.get("_rsc"); // null = parameter missing
  if (type.startsWith("text/html")) {
    return rsc === null && !ROUTER_HEADERS.some((name) => req.headers.has(name));
  }
  if (type.startsWith("text/x-component")) {
    return req.headers.get("rsc") === "1" && rsc !== null && rsc === expectedRscParam(req);
  }
  return false;
}

/* ---------- request passed to Next.js on a MISS ----------
 * The cache key ignores cookies and Authorization, so always render the anonymous variant.
 * Conditional headers are dropped so the origin returns a full 200 for the edge to store
 * (the edge answers If-None-Match with 304 by itself). Router headers stay: Next.js needs
 * them to build the right RSC payload.
 */
function upstreamRequest(request, url) {
  if (request.method !== "GET" || NEVER_CACHE.some((p) => url.pathname.startsWith(p))) return request;
  const h = new Headers(request.headers);
  for (const name of ["Cookie", "Authorization", "If-None-Match", "If-Modified-Since"]) h.delete(name);
  return new Request(request, { headers: h });
}

/* ---------- response headers = cache policy ---------- */

function noStore(h) {
  h.set(EDGE_CC, NO_STORE);
  h.set("Cache-Control", NO_STORE); // also stops other shared caches from keeping Next's s-maxage
}

// Next.js on Workers returns uncompressed bodies and Cloudflare compresses at the edge,
// so Vary: Accept-Encoding would only split the cache into identical variants.
function dropAcceptEncodingVary(h) {
  if (h.has("Content-Encoding")) return;
  const vary = (h.get("Vary") || "")
    .split(",")
    .map((v) => v.trim())
    .filter((v) => v && v.toLowerCase() !== "accept-encoding");
  if (vary.length) h.set("Vary", vary.join(", "));
  else h.delete("Vary");
}

function applyCachePolicy(request, url, resp) {
  const out = new Response(resp.body, resp); // mutable copy
  const h = out.headers;
  const type = (h.get("Content-Type") || "").toLowerCase();
  const isPage = type.startsWith("text/html") || type.startsWith("text/x-component");
  const cacheableRequest =
    request.method === "GET" &&
    url.hostname === SITE_HOST &&
    !NEVER_CACHE.some((p) => url.pathname.startsWith(p));

  if (isPage) {
    const originCc = (h.get("Cache-Control") || "").toLowerCase();
    const store =
      cacheableRequest &&
      out.status === 200 &&
      !h.has("Set-Cookie") &&
      !originCc.includes("private") &&
      !originCc.includes("no-store") &&
      isCanonicalPage(request, url, type);

    if (!store) {
      noStore(h);
      return out;
    }
    h.set(EDGE_CC, EDGE_PAGE_CC);
    h.set("Cache-Control", CLIENT_PAGE_CC);
    h.set("Cache-Tag", type.startsWith("text/html") ? "html" : "rsc"); // for targeted purge, not sent to the browser
    dropAcceptEncodingVary(h);
    return out;
  }

  // Everything else (API, sitemaps, redirects, errors). Without an explicit Cache-Control,
  // Workers Cache applies heuristic TTLs (200 for 2 h, 301 for 20 min, 404 for 3 min).
  if (!cacheableRequest || out.status >= 400 || !h.has("Cache-Control")) noStore(h);
  return out;
}

/* ---------- POST /__cache-purge (token in CACHE_PURGE_TOKEN) ---------- */

async function sha256(text) {
  return new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)));
}

async function tokenMatches(request, expected) {
  const auth = request.headers.get("Authorization") || "";
  const given = auth.startsWith("Bearer ") ? auth.slice(7) : "";
  // Hash both sides first: equal lengths, so the comparison runs in constant time.
  const [a, b] = await Promise.all([sha256(given), sha256(expected)]);
  return crypto.subtle.timingSafeEqual(a, b);
}

async function handlePurge(request, env, ctx) {
  const headers = { "Cache-Control": NO_STORE, [EDGE_CC]: NO_STORE };
  if (!env.CACHE_PURGE_TOKEN || request.method !== "POST") return new Response("Not Found", { status: 404, headers });
  if (!(await tokenMatches(request, env.CACHE_PURGE_TOKEN))) return new Response("Unauthorized", { status: 401, headers });

  const body = await request.json().catch(() => ({}));
  const scope = {};
  if (Array.isArray(body.tags) && body.tags.length) scope.tags = body.tags;
  if (Array.isArray(body.pathPrefixes) && body.pathPrefixes.length) scope.pathPrefixes = body.pathPrefixes;

  const result = await ctx.cache.purge(Object.keys(scope).length ? scope : { purgeEverything: true });
  return Response.json(result, { status: result.success ? 200 : 500, headers });
}

/* ---------- entry ---------- */

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    if (url.pathname === "/__cache-purge") return handlePurge(request, env, ctx);

    const resp = await openNextWorker.fetch(upstreamRequest(request, url), env, ctx);
    return applyCachePolicy(request, url, resp);
  },
};

Používáte cookies pro přihlášené uživatele? Pak pro tyto cesty Cookie neodstraňujte: přidejte je do NEVER_CACHE.

Krok 4: Vytvořte token pro purge

Bez secretu CACHE_PURGE_TOKEN endpoint pro purge neexistuje (vrací 404). Spusťte ve složce projektu (Linux, macOS nebo WSL):

Terminál
# generate a random token into .env (.env must be in .gitignore)
echo "CACHE_PURGE_TOKEN=$(openssl rand -hex 32)" >> .env

# store the same value as a worker secret
grep '^CACHE_PURGE_TOKEN=' .env | cut -d= -f2 | npx wrangler secret put CACHE_PURGE_TOKEN

Krok 5: Build a deploy

Terminál
npx opennextjs-cloudflare build
npx opennextjs-cloudflare deploy

Pak projděte kontroly v kapitole Ověření. První request na stránku je MISS, druhý HIT.

Přesměrování bez www → www patří před cache

Přesměrování napsané ve Workeru (třeba example.com → www.example.com) se při HIT přeskočí, protože host není součástí klíče. Použijte místo něj Redirect Rule zóny v dashboardu Cloudflare. Ta běží před Workers Cache.

03 · Pravidla cache

Co cachovat a jak dlouho

Cache Rules, Page Rules ani Cache Reserve z nastavení zóny se na Workers Cache neuplatňují. Co se uloží a na jak dlouho, rozhodují výhradně hlavičky odpovědi, kterou Worker vrátí.

Doporučená pravidla

Cesta / odpověďEdge (Cloudflare)Prohlížeč
HTML stránka, kanonický request7 dní + 1 den SWR + 1 den stale-if-error10 min
RSC payload od routeru, kanonický request7 dní + 1 den SWR + 1 den stale-if-error10 min
HTML nebo RSC, které nejsou kanonickéneukládá seno-store
404 a jiné ne-200 stránkyneukládá seno-store
/api/*neukládá seno-store
Ostatní odpovědi (sitemapy, přesměrování)podle vlastního Cache-Control, bez něj se neukládápodle Next.js
Jiný host než produkčníneukládá se-
Jiná metoda než GETneukládá se-

Dvě hlavičky: edge a prohlížeč

Cloudflare čte pravidla cache ze tří hlaviček v tomto pořadí priority: Cloudflare-CDN-Cache-Control, CDN-Cache-Control a Cache-Control. První z nich se před odesláním k návštěvníkovi odstraní. Díky tomu může edge držet stránku 7 dní a prohlížeč jen 10 minut.

HTTP
Cloudflare-CDN-Cache-Control: max-age=604800, stale-while-revalidate=86400, stale-if-error=86400
Cache-Control: public, max-age=600
Cache-Tag: html
  • Cloudflare-CDN-Cache-Control - jen pro edge, nejvyšší priorita, návštěvník ji nevidí.
  • Cache-Control - jde k návštěvníkovi.
  • Cache-Tag - štítky pro cílený purge (html, rsc), návštěvník je také nevidí.

Život jedné stránky na edge

Život jedné stránky v cache: 7 dní čerstvá, další den se vrací stará kopie a na pozadí se obnovuje (nebo když render selže), po 8 dnech se zahodí. Prohlížeč si kopii drží jen 10 minut.Edge (Workers Cache)Čerstvá: servíruje se z cache, Worker neběžízahozeno07 dní8 dníSWR + stale-if-errorstará kopie hned,nový render na pozadídalší návštěvačeká na renderProhlížeč10 min, pak 304 z edge (≈ 50 ms, bez Workeru)
7 dní je horní hranice, ne záruka. Méně navštěvované stránky může Cloudflare vyhodit dřív.

S ukázkovými hodnotami prochází každá stránka na edge třemi fázemi:

  • 0-7 dní - čerstvá. Servíruje se z cache, Worker neběží.
  • 7-8 dní - stará, ale použitelná. SWR: první návštěvník dostane starou kopii okamžitě a na pozadí se stránka vyrenderuje znovu. stale-if-error: když render selže (databáze neodpovídá, 5xx), vrátí se uložená kopie.
  • Po 8 dnech - zahozená. Další návštěva čeká na render.

Proč jen 10 minut v prohlížeči

Kopii na edge jde kdykoli zahodit (deploy, rollback, purge), kopii v prohlížeči ne. Když se ven dostane rozbitá stránka, u návštěvníka vydrží nejvýš 10 minut.

Při HIT posílá Cloudflare i hlavičku Age (jak dlouho je kopie v cache) a prohlížeč si ji odečte od max-age. Kopie starší než 10 minut se proto při další návštěvě ověří a odpoví se 304 Not Modified přímo z edge (kolem 50 ms, bez Workeru). Je to levné a v pořádku.

Pasti v hlavičkách

PastCo se staneCo dělat
Chybí Cache-ControlUloží se i tak podle heuristiky: 200 na 2 hodiny, 301 na 20 minut, 404 na 3 minuty.Vše, co se cachovat nesmí, musí mít explicitně no-store.
s-maxage, must-revalidate, proxy-revalidateVypnou stale-while-revalidate. Vypršená stránka čeká na render.Next.js posílá s-maxage u statických stránek. Pravidlo pro edge nastavte přes max-age v Cloudflare-CDN-Cache-Control.
Set-Cookie v odpovědiAutomatický BYPASS, Worker běží pokaždé.Na veřejných stránkách cookies nenastavujte (lišta souhlasu, A/B testy řešte na klientovi).
Authorization v požadavkuBYPASS, pokud odpověď není public. Pak se uloží a sdílí.Před renderem Authorization odstraňte (ukázka to dělá), uložená kopie je pak vždy anonymní.
Vary: Accept-EncodingRozdělí cache na stejné varianty podle prohlížeče.Next.js na Workers vrací nekomprimované tělo a komprimuje až edge, proto wrapper odstraní accept-encoding z Vary.
Zapamatujte si
  • Na edge dlouho, v prohlížeči krátce: edge jde vyčistit, prohlížeč ne.
  • Pravidlo pro edge v Cloudflare-CDN-Cache-Control s max-age, nikdy s-maxage.
  • Bez hlavičky neznamená bez cache. Dynamické odpovědi označte no-store.

04 · Klíč cache

Klíč cache a otrávení cache

Z pohledu bezpečnosti nejdůležitější kapitola. Klíč cache je mnohem jednodušší než u klasické CDN. Když dvě různé odpovědi sdílejí jednu URL, cache je může zaměnit a k rozbití stránky pro všechny stačí jeden obyčejný request.

Co je v klíči

V klíči jeV klíči není
cesta + query string (záleží na pořadí parametrů)host
verze Workeru: každý deploy, rollback i wrangler secret put začíná s prázdnou cachecookies
cílový entrypoint WorkeruAuthorization ani jiné hlavičky požadavku
metoda: GET a HEAD sdílí jednu položku
Na Vary se nespoléhejte

Dokumentace uvádí, že Workers Cache respektuje všechny názvy v hlavičce Vary. V praxi se RSC payload pod adresu stránky uložil, přestože Next.js posílá Vary: rsc, next-router-state-tree, …. Variantu musí určovat samotná URL.

Co se stalo: RSC pod adresou stránky

Otrávení cache: jeden požadavek s hlavičkou RSC: 1 na holou adresu stránky. Bez ochrany se RSC payload uloží pod klíčem HTML a každý návštěvník dostane surová data. S ochranou se odpověď neuloží a HTML zůstane nedotčené.BEZ OCHRANYGET /cenik + RSC: 1→ text/x-componentuloží se pod /ceniksurové RSC pro všechnyS OCHRANOUGET /cenik + RSC: 1→ text/x-componentnení kanonická → no-storeHTML zůstane nedotčené
Takový request může poslat kdokoli, omylem i schválně. Proto ochrana patří do každého nastavení, nejen kvůli ladění.

Při ladění šel na produkci request s hlavičkou RSC: 1 na holou adresu stránky, bez ?_rsc=. Next.js správně vrátil RSC payload (text/x-component). Workers Cache ho uložil pod adresu stránky a návštěvníci po celou dobu TTL dostávali místo stránky surový text payloadu.

Stačil k tomu jeden obyčejný request s jednou hlavičkou. Cache obě odpovědi nerozliší, protože mají stejnou URL, a tedy i stejný klíč.

Ochrana: ukládat jen kanonickou odpověď

Odpověď uložte jen tehdy, když je přesně taková, jakou by pod danou URL čekal běžný návštěvník (isCanonicalPage v ukázce):

  • HTML jen pro request bez _rsc a bez jakékoli hlavičky routeru (rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, next-url).
  • RSC jen s RSC: 1 a s hodnotou _rsc, která přesně odpovídá hashi hlaviček routeru. Next.js ho počítá jako djb2 z prefetch,segmentPrefetch,stateTree,nextUrl, v base36, prvních 5 znaků. Next.js tuhle kontrolu sám má (experimental.validateRSCRequestHeaders), ale pod OpenNext neběží, proto ji dělá wrapper.
  • A navíc jen: produkční host, GET, status 200, bez Set-Cookie, bez private či no-store od originu.

Všechno ostatní se návštěvníkovi obslouží normálně, jen s no-store v obou hlavičkách. Druhá z nich (Cache-Control) brání tomu, aby s-maxage z Next.js podržela jiná sdílená cache po cestě.

Výsledek: request z incidentu se obslouží jako BYPASS a HTML pod stejnou URL zůstane nedotčené. Podvržený hash _rsc se také neuloží.

Testujte na vlastních URL

Testy, které by mohly cache otrávit, pouštějte na URL s unikátním parametrem (?cachetest=<náhodné číslo>). Ta má vlastní klíč a skutečné stránky neovlivní.

05 · Purge

Purge bez deploye

Purge v dashboardu Cloudflare (ani přes API zóny či Terraform) na Workers Cache nedosáhne. Mazat jde jen z Workeru přes ctx.cache.purge(), a to výhradně cache tohoto Workeru. Ukázka ho zpřístupňuje jako endpoint chráněný tokenem.

Endpoint POST /__cache-purge

Terminál
export CACHE_PURGE_TOKEN=$(grep '^CACHE_PURGE_TOKEN=' .env | cut -d= -f2)

# everything
curl -X POST https://www.example.com/__cache-purge \
  -H "Authorization: Bearer $CACHE_PURGE_TOKEN"

# by path prefix (the page and everything below it)
curl -X POST https://www.example.com/__cache-purge \
  -H "Authorization: Bearer $CACHE_PURGE_TOKEN" \
  -d '{"pathPrefixes":["/blog"]}'

# by tag: only RSC payloads (html = documents)
curl -X POST https://www.example.com/__cache-purge \
  -H "Authorization: Bearer $CACHE_PURGE_TOKEN" \
  -d '{"tags":["rsc"]}'
  • Bez těla smaže vše, pathPrefixes smaže stránku a vše pod ní, tags maže podle Cache-Tag.
  • Bez tokenu nebo se špatným tokenem: 401. Na GET: 404. Bez secretu endpoint neexistuje (404).
  • Token se porovnává v konstantním čase (obě strany přes SHA-256, pak timingSafeEqual).
  • Purge má limit počtu volání podle Free plánu purge zóny, a to na každém plánu. Volejte ho jednou po dávce změn, ne po každé změně.

Změny obsahu

Změny v databázi (nové články, úpravy, pořadí) se samy projeví až po vypršení kopie na edge, tedy do 7 dní (v krajním případě 8). Po dávce změn zavolejte purge: celý, nebo jen dotčené pathPrefixes. Nejlepší místo je akce uložení ve vašem CMS nebo administraci.

Další způsoby, jak cache vyprázdnit

  • Deploy nebo rollback: nová verze znamená nový klíč.
  • wrangler secret put také vytvoří novou verzi, cache tedy začne prázdná.
Co zmůže uniklý token

Jen vyprázdnit cache. Nečte data a nemění obsah. Opakované mazání by cache drželo prázdnou: vyšší CPU, vyšší účet, pomalejší web, případně vyčerpaný limit purge. Řešením je token vyměnit. Nejdřív z .env smažte starý řádek, pak znovu spusťte krok 4 z kapitoly Zapnutí.

06 · Deploy

Deploy, rollback a staré chunky

Nastavení cache je součástí verze Workeru. Deploy platí okamžitě a nikdy nevrátí HTML, které odkazuje na staré chunky, hit rate ale pokaždé začíná od nuly.

Změny wrapperu nepotřebují rebuild

Build OpenNext vyrábí jen .open-next/, worker-entry.mjs sbalí až Wrangler při deployi. Změny ve wrapperu nebo ve wrangler.jsonc proto stačí nasadit. Rebuild je potřeba jen po změnách v src/, next.config.ts a podobně.

Terminál
# only worker-entry.mjs or wrangler.jsonc changed: no rebuild needed
npx opennextjs-cloudflare deploy

Rollback

Terminál
npx wrangler rollback   # previous version, including its cache settings
  • Rollback vrátí i nastavení cache dané verze.
  • "enabled": false cache nevypne zpětně: uložené položky nesmaže a použije je znovu, pokud se cache zase zapne.

cross_version_cache nechte vypnuté

cross_version_cache: true drží zahřátou cache přes deploye. Stará HTML by ale odkazovala na soubory v _next/static, které nový build už nemusí mít, a stránka spadne s chybou ChunkLoadError. Pokud ho přesto zapnete, po každém deployi, který mění obsah, zavolejte purge.

Staré buildy v .open-next/assets/_next/static držte v každém případě. Prohlížeče mají HTML až 10 minut a otevřené záložky donačítají chunky ze své verze. Viz slučování starých chunků _next/static.

07 · Ověření

Jak ověřit, že to funguje

Při HIT nevznikají logy Workeru, hlavním nástrojem je proto hlavička odpovědi Cf-Cache-Status. Hit ratio a počty HIT / MISS / BYPASS ukazuje i Workers Observability.

Kontroly po deployi

Terminál
# GET twice (not curl -I: only GET responses are stored)
curl -s -o /dev/null -D - https://www.example.com/ | grep -i -E "cf-cache-status|^cache-control"
curl -s -o /dev/null -D - https://www.example.com/ | grep -i -E "cf-cache-status|^cache-control"
Terminál
# a unique URL of its own, so a failed test cannot poison a real page
T="https://www.example.com/?cachetest=$RANDOM"
curl -s -o /dev/null -D - -H "RSC: 1" "$T" | grep -i -E "cf-cache-status|content-type"   # BYPASS, text/x-component
curl -s -o /dev/null -D - "$T" | grep -i -E "cf-cache-status|content-type"               # text/html
KontrolaJakOčekávání
Stránka se cachujedvakrát GET na stejnou URLMISS, pak HIT; Cache-Control: public, max-age=600
Ochrana proti otráveníRSC: 1 na holou URL, pak stejná URL bez hlavičkynejdřív BYPASS + text/x-component, pak text/html
API se necachujelibovolná URL /api/…BYPASS, Cache-Control: no-store
404 se necachujeneexistující URLBYPASS, no-store
RevalidaceIf-None-Match: "<etag>" na cachovanou stránku304, HIT
Interní hlavičky neunikajíhlavičky libovolné stránkybez Cloudflare-CDN-Cache-Control a Cache-Tag
Klientská navigaceproklikat web s otevřenými DevToolsbez chyb v konzoli

Hodnoty Cf-Cache-Status

HodnotaVýznam
HITVráceno z cache, Worker neběžel.
MISSV cache nebylo: Worker běžel a odpověď se uložila (pokud to pravidla dovolí).
EXPIREDKopie vypršela a vyrenderovala se znovu.
REVALIDATEDVypršená kopie byla potvrzena jako stále platná.
UPDATINGstale-while-revalidate: vrátila se stará kopie a nová se renderuje na pozadí.
STALEVrátila se stará kopie, protože render selhal (stale-if-error).
BYPASSNeukládá se: no-store, Set-Cookie, Authorization nebo jiná metoda než GET.

08 · Přechod

Z vlastní vrstvy nad Cache API na Workers Cache

Před Workers Cache se vyrenderované stránky běžně cachovaly wrapperem, který HTML a RSC ukládal do caches.default (Cache API). Pokud takové řešení dnes provozujete, přechod znamená hlavně mazání kódu.

Cache API (caches.default ve wrapperu)Workers Cache
Kde cache jeuvnitř Workeru, který se spouští při každém requestupřed Workerem, při HIT Worker neběží
Dosahjedno datacentrum, každé se plní zvlášťnižší vrstva + vyšší vrstva sdílená celou sítí
Klíčvlastní (hash z _rsc, hlaviček routeru, id deploye)cesta + query + verze Workeru
Po vypršeníprvní návštěvník čeká na renderSWR: stará kopie hned, obnova na pozadí
Purgenapříč datacentry nejde, jen čekat na TTLctx.cache.purge()
Po deployistudená cache (id deploye v klíči)studená cache (verze v klíči)
Platba při zásahurequest + CPUjen request

Přechod ve čtyřech krocích

  1. Z worker-entry.mjs odstraňte dotazy do caches.default, vlastní klíče, hashování a přepisování Vary.
  2. Zapněte cache ve wrangler.jsonc (krok 2).
  3. Pravidla cache nastavte v hlavičkách odpovědi, včetně ochrany variant (krok 3).
  4. Ověřujte přes Cf-Cache-Status místo vlastní hlavičky typu X-Edge-Cache (Ověření).
Cache API má dál své místo

Pro cachování dat uvnitř Workeru, například lokální vrstvu v datacentru před R2 bucketem, zůstává Cache API správným nástrojem. Viz R2 storage a cache (anglicky).

Zdroje

Oficiální dokumentace a další čtení

Chcete rychlejší web na Cloudflare?

Nastavíme Workers Cache, OpenNext i celý stack Cloudflare pro váš projekt.

Rezervovat schůzku