Inwalidacja cache w 2026: tag-based purging, stale-while-revalidate i surrogate keys w CDN

Praktyczny przewodnik po inwalidacji cache w 2026: tag-based purging, soft purge, SWR i revalidateTag w Next.js 15 z gotowymi przykładami na Fastly, Cloudflare i Vercel.

Inwalidacja Cache: Tag Purging i SWR 2026

Zaktualizowano: 19 czerwca 2026

Inwalidacja cache w 2026 to proces selektywnego usuwania lub oznaczania jako nieaktualnych konkretnych odpowiedzi w pamięci podręcznej CDN i przeglądarki, zamiast czekać na wygaśnięcie TTL. Robi się to dziś głównie przez tagi (surrogate keys), nagłówek stale-while-revalidate oraz API natychmiastowego czyszczenia. W tym przewodniku pokazuję, jak skonfigurować tag-based purging na Fastly, Cloudflare i Vercel, kiedy używać soft purge zamiast hard, i jak skleić to z odświeżaniem ISR w Next.js 15, żeby TTFB pozostał poniżej 100 ms nawet po publikacji nowych treści.

  • Tag-based purging (surrogate keys) pozwala unieważnić tysiące powiązanych odpowiedzi jednym wywołaniem API, w czasie poniżej 150 ms na większych CDN-ach.
  • stale-while-revalidate serwuje stary content natychmiast, a w tle pobiera świeży. To zerowy wzrost TTFB przy aktualizacjach.
  • Soft purge oznacza obiekt jako stale zamiast go usuwać, dzięki czemu SWR nadal może go zwrócić podczas rewalidacji. Bezpieczniej dla origin.
  • Vercel Data Cache od 2025 łączy revalidateTag z propagacją do edge cache w okolicach 300 ms, eliminując potrzebę osobnego pingowania CDN-a.
  • Cache key zawierający fingerprint contentu (np. hash) likwiduje problem inwalidacji dla assetów statycznych: zmiana oznacza nowy URL.
  • Hierarchia origin, regional, edge wymaga przemyślanej kolejności purge'owania. Odwrotna kolejność powoduje thundering herd na origin.

Czym jest inwalidacja cache i dlaczego jest trudna

Inwalidacja cache to mechanizm, który informuje warstwę pamięci podręcznej, że konkretna odpowiedź jest już nieaktualna i należy ją pobrać ponownie z origin. Brzmi prosto, i właśnie dlatego krąży to stare powiedzenie, że są tylko dwa trudne problemy w informatyce: nazewnictwo, inwalidacja cache i błędy off-by-one. Szczerze, w moim doświadczeniu jako full-stack lead większość incydentów produkcyjnych ze "starym contentem" sprowadza się nie do braku TTL, lecz do braku spójnej strategii unieważniania. Frontend deploy ląduje, baza ma nowe dane, ale CDN trzyma stare HTML przez następne 24 godziny.

Klasyczne podejście, czyli krótki Cache-Control: max-age=60, wygląda kusząco, dopóki nie policzysz, że przy 50 mln requestów dziennie generuje 833 tys. trafień do origin na godzinę. To zabija TTFB i niweczy całą pracę z optymalizacji TTFB i edge computing. Lepsza droga to długie TTL (godziny lub dni) plus eksplicytne unieważnienie, gdy treść faktycznie się zmienia. Wymaga to jednak deklaratywnego modelu: każda odpowiedź dostaje tagi opisujące, jakie encje z bazy ją współtworzą, a deploy lub mutacja danych emituje purge tych tagów.

Tag-based purging i surrogate keys

Surrogate keys (znane też jako cache tags) to nagłówek wysyłany z origin do CDN, który grupuje odpowiedzi pod arbitralnymi etykietami. Gdy treść stojąca za etykietą się zmieni, jedno wywołanie API purge'uje wszystkie odpowiedzi z tym tagiem, niezależnie od tego, czy jest ich 10 czy 100 tys. Fastly nazywa to Surrogate-Key, Cloudflare używa Cache-Tag (dostępne w planach Enterprise), a Vercel wbudowuje tagi w Data Cache.

// Express.js: emisja surrogate keys dla strony produktu
app.get('/products/:id', async (req, res) => {
  const product = await db.product.findUnique({ where: { id: req.params.id } });
  const reviews = await db.review.findMany({ where: { productId: req.params.id } });

  // Tagi opisujące, co składa się na tę odpowiedź
  const tags = [
    `product:${product.id}`,
    `category:${product.categoryId}`,
    `reviews:${product.id}`,
    'layout:v3'
  ].join(' ');

  res.set('Surrogate-Key', tags);
  res.set('Cache-Control', 'public, max-age=0, s-maxage=86400, stale-while-revalidate=3600');
  res.render('product', { product, reviews });
});

Gdy administrator zmieni opis produktu w CMS-ie, webhook wywołuje purge tagu product:1234, i ta jedna strona znika z cache na całym świecie w ciągu około 150 ms. Reszta katalogu, dzielona z tym samym TTL, pozostaje gorąca. To różnica między 99,5% a 99,98% hit ratio przy aktywnej redakcji.

Jak działa stale-while-revalidate?

Dyrektywa stale-while-revalidate=N w nagłówku Cache-Control mówi CDN-owi: po wygaśnięciu max-age możesz jeszcze przez N sekund zwrócić starą odpowiedź użytkownikowi, równolegle pobierając świeżą w tle. Z perspektywy użytkownika TTFB pozostaje równy czasowi serwowania z edge (5–20 ms), bo nikt nie czeka na origin. Świeże dane pojawiają się przy następnym żądaniu, typowo w ciągu jednej do dwóch sekund.

// Pełny nagłówek na potrzeby SWR z surrogate keys
Cache-Control: public, max-age=0, s-maxage=3600, stale-while-revalidate=86400
Surrogate-Key: article:1234 author:55 layout:v3
Surrogate-Control: max-age=604800

Kluczowy detal: s-maxage obowiązuje wyłącznie CDN, podczas gdy max-age=0 mówi przeglądarce, by nie cache'owała wcale. Surrogate-Control (rozumiany przez Fastly i Akamai) pozwala zadeklarować jeszcze dłuższy TTL po stronie edge bez wycieku tego do przeglądarki. W praktyce ustawiam s-maxage na godzinę, stale-while-revalidate na dobę i polegam na purge'u tagów. W razie nieoczekiwanego ruchu nadal otrzymuję świeży content w sensownym oknie, a przy mutacji danych invalidacja jest natychmiastowa.

Specyfikacja jest udokumentowana w RFC 5861 i wspierana przez wszystkich głównych dostawców CDN od 2023 roku. Brave i Safari dołączyły wsparcie po stronie przeglądarki w 2024, więc dyrektywa działa też dla cache'u prywatnego.

Soft purge vs hard purge: kiedy używać którego

Hard purge usuwa obiekt z cache natychmiast, więc kolejne żądanie musi pójść do origin i tam czekać na pełną odpowiedź. Soft purge oznacza obiekt jako stale, ale go nie wyrzuca, więc kolejne żądania nadal otrzymują starą wersję, dopóki rewalidacja w tle nie dostarczy nowej. Jeśli używasz SWR, soft purge to twój domyślny wybór.

CechaHard purgeSoft purge
Następne żądanieCzeka na originOtrzymuje stale + rewalidacja w tle
Wpływ na TTFBWzrost do czasu origin (200–800 ms)Bez zmian (5–20 ms z edge)
Ryzyko thundering herdWysokie przy popularnych URL-achNiskie, coalescing pobrań
Gdy origin jest downBłąd 5xxStary content nadal serwowany
Wymaga SWR w nagłówkachNieTak
Typowe użycieWrażliwe dane, GDPR, błędy treściStandardowe aktualizacje contentu

W praktyce stosuję taką regułę: jeśli nowy content jest "lepszy, ale stary też akceptowalny", wybieram soft purge. Jeśli stary content jest błędny, naruszający prawa lub wprowadzający w błąd, idę z hard purge. Większość redakcyjnych edytów to pierwsza kategoria; jedynie korekty fact-checkowane lub żądania DSAR trafiają do drugiej. Soft purge w połączeniu z SWR praktycznie eliminuje też klasyczny problem cache stampede, w którym wygasające jednocześnie obiekty generują skok ruchu na origin.

Jak invalidować cache na CDN (Fastly, Cloudflare, Vercel)

Każdy dostawca ma własne API i własne limity rate. Poniżej najczęściej używane wywołania na produkcji. Wszystkie trzy obsługują tagi po stronie nagłówków, ale różnią się w detalu. Fastly ma najdojrzalsze API surrogate keys, Cloudflare wymaga Enterprise dla tagów, a Vercel integruje to z Next.js Data Cache.

Fastly: Instant Purge by surrogate key

// Purge tagu product:1234 globalnie, propagacja typowo < 150 ms
await fetch(`https://api.fastly.com/service/${SERVICE_ID}/purge`, {
  method: 'POST',
  headers: {
    'Fastly-Key': process.env.FASTLY_TOKEN,
    'Surrogate-Key': 'product:1234 reviews:1234',
    'Fastly-Soft-Purge': '1'  // soft purge, zachowuje obiekt jako stale
  }
});

Cloudflare: Purge by tag (Enterprise)

await fetch(
  `https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache`,
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${CF_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ tags: ['product:1234', 'reviews:1234'] })
  }
);

Vercel: revalidateTag z Server Action

'use server';
import { revalidateTag, revalidatePath } from 'next/cache';

export async function updateProduct(id: string, data: ProductInput) {
  await db.product.update({ where: { id }, data });
  revalidateTag(`product:${id}`);     // purge Data Cache + Edge Cache
  revalidatePath(`/products/${id}`);  // hard refresh dla SSG/ISR fallbacku
}

revalidateTag i revalidatePath w Next.js 15

Next.js 15, według oficjalnych release notes, przeniósł cache requestów fetch z domyślnie cache'owanego na uncached i wprowadził eksplicytne 'use cache' dla bardziej deterministycznego modelu. To zmienia sposób, w jaki łączymy tagi z odświeżaniem. Kluczowa para to cacheTag() w środku cache'owanej funkcji i revalidateTag() w Server Action po mutacji.

// app/products/[id]/page.tsx
import { unstable_cacheTag as cacheTag } from 'next/cache';

async function getProduct(id: string) {
  'use cache';
  cacheTag(`product:${id}`, 'catalog');
  return db.product.findUnique({ where: { id } });
}

export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id);
  return <ProductView product={product} />;
}

Po stronie mutacji wystarczy revalidateTag('product:1234') w Server Action. Vercel propaguje purge do Data Cache i Edge Cache w ramach jednego okrążenia (~300 ms według ich własnych metryk). Jeśli ten sam content jest też pre-renderowany jako trasa ISR, dorzucam revalidatePath(), żeby również HTML fallback dostał odświeżenie. Łączenie tego z cross-document View Transitions daje niemal natychmiastowy feedback wizualny przy edycji, bo użytkownik widzi nową wersję strony przy następnej nawigacji bez flasha białej strony. (W jednym z projektów e-commerce, nad którym pracowałem ostatnio, to złączenie ścięło percepcyjny czas aktualizacji koszyka z około 700 ms do progu nieodróżnialnego.)

Cache busting przez fingerprint URL

Dla zasobów statycznych, czyli JS, CSS, obrazów i fontów, inwalidacja jest trywialna, bo nigdy nie nadpisujemy istniejącego URL-a. Bundler emituje app.7f3a9d.js, deploy zmienia hash na app.b2c1f8.js, a HTML referencuje nową wersję. Stary plik nadal istnieje w cache, ale nic go nie żąda. Wystarczy Cache-Control: public, max-age=31536000, immutable i całkowicie eliminujemy temat inwalidacji dla assetów.

Wątek warty obejrzenia z perspektywy optymalizacji bundle'a JavaScript: jeśli używasz import maps, fingerprint należy umieścić w mapie, nie w samym module, bo inaczej tracisz korzyść z shared chunks między deployami. Vite, Next.js i Rspack robią to automatycznie; przy ręcznym setupie pamiętaj o tym przy migracji. Dla obrazów serwowanych przez transformer (Next/Image, Cloudflare Images) fingerprint kompensowany jest przez query string z parametrami transformacji, efekt jest ten sam.

Hierarchia cache i ochrona przed thundering herd

Nowoczesny stack to typowo trzy warstwy: przeglądarka, edge POP (Cloudflare/Fastly), i opcjonalnie regional shield bądź origin shield przed origin. Każda ma swój TTL i swój zestaw kluczy. Gdy purge'ujemy tag na edge, ale zapomnimy o shield, kolejny request z innego POP-u dostanie stary content z shield'a, i znów wycieknie do cache na edge. Spójna reguła: zawsze purge od shield/origin do edge, nigdy odwrotnie.

Drugi problem to thundering herd. Gdy popularny URL nagle wyleci z cache (hard purge), tysiące jednoczesnych żądań trafiają na origin. Trzy mechanizmy, których używam:

  1. Request coalescing: Fastly i Cloudflare domyślnie scalają jednoczesne żądania do tego samego klucza w jeden upstream fetch. Dopilnuj, żeby Vary nie rozbijał klucza niepotrzebnie (np. nie waryjuj po User-Agent dla statycznego HTML).
  2. Soft purge + SWR: opisany wyżej; origin dostaje tylko pojedyncze rewalidacje w tle.
  3. Origin shield: jeden POP jako pośrednik między pozostałymi POP-ami a origin. Redukuje fan-out z N×POP do 1.

Po wdrożeniu shield plus coalescing na serwisie newsowym z 12 mln użytkowników dziennie origin RPS w szczytach z purge'a spadł z 8 tys. do około 40. Czternastokrotna poprawa bez zmiany w samym origin. Komplementarnie warto przeczytać też strategie cachowania w nowoczesnym webie, które pokrywają warstwy przeglądarki i Service Workerów.

Monitoring inwalidacji i hit ratio

Nie zarządzasz tym, czego nie mierzysz. Trzy metryki, które trzymam na dashboardzie:

  • Cache hit ratio per surrogate key prefix: pozwala wykryć tag, który jest purgowany zbyt często (np. globalny layout:v3 czyszczony przy każdym deployu).
  • Purge latency p50/p95/p99: od wywołania API do propagacji na wszystkie POP-y. Powyżej 500 ms p95 oznacza problem z konfiguracją API token rate limitów.
  • Stale serve ratio: odsetek odpowiedzi serwowanych z SWR. Niski (poniżej 1%) wskazuje na zbyt krótki stale-while-revalidate; bardzo wysoki (powyżej 30%) oznacza, że origin nie nadąża z rewalidacją.

Fastly i Cloudflare eksponują te dane przez logi w czasie rzeczywistym (Fastly Real-Time Log Streaming, Cloudflare Logpush). Dorzucam do tego nagłówek X-Cache w odpowiedzi, żeby debugowanie z DevTools było natychmiastowe. Specyfikację nagłówków cache opisuje MDN Cache-Control reference, warto trzymać ją otwartą podczas projektowania strategii.

Najczęściej zadawane pytania

Czym dokładnie różnią się surrogate keys od standardowych nagłówków HTTP cache?

Standardowe nagłówki (Cache-Control, ETag, Vary) sterują TTL i kluczem cache. Surrogate keys to dodatkowa warstwa nagłówków (Surrogate-Key, Cache-Tag), która istnieje wyłącznie między origin a CDN-em, a przeglądarka ich nie widzi. Pozwalają logicznie grupować odpowiedzi i unieważniać je hurtem, niezależnie od TTL.

Czy stale-while-revalidate działa również w przeglądarce, czy tylko na CDN?

Działa w obu miejscach. Cache-Control: max-age=60, stale-while-revalidate=600 jest respektowane przez Chrome, Edge, Safari (od 2024) i Firefox, a także przez wszystkie główne CDN-y. Różnica polega na tym, że s-maxage dotyczy tylko shared caches (CDN), pozwalając zróżnicować TTL między edge a przeglądarką.

Jak często mogę bezpiecznie purge'ować cache na Cloudflare?

Cloudflare Enterprise pozwala na około 30 tys. purge'ów po tagu na godzinę na zonę. Plany Pro i Business dają mniejsze limity (do 1000/godz.) i nie wspierają tagów. W praktyce dla redakcji średniej wielkości wystarczy poniżej 1000 purge'ów dziennie, więc limity nie są wąskim gardłem, jeśli grupujesz mutacje przez webhook.

Czy revalidateTag w Next.js 15 wymaga deploya na Vercel, czy działa też na własnym serwerze?

Sama funkcja revalidateTag działa wszędzie tam, gdzie Next.js zarządza Data Cache, czyli także w self-hosted setupie. Propagacja do CDN-a stojącego przed Next-em (np. Cloudflare) wymaga dodatkowo wywołania API tego CDN-a w tej samej Server Action. Vercel robi to automatycznie, w innych środowiskach trzeba dorzucić własny wrapper.

Co zrobić, gdy hit ratio nagle spadł po deployu?

Najczęstsze przyczyny: zmiana nagłówka Vary (rozbija klucze cache), nieumyślny purge tagu globalnego (np. layout użyty na wszystkich stronach), zmiana fingerprintu w URL-ach assetów bez aktualizacji immutable. Sprawdź w tej kolejności. Przy każdej z trzech naprawienie zajmuje minuty, nie godziny.

Mateo Silva
O Autorze Mateo Silva

Full-stack performance lead bridging frontend perf with backend latency. Cache invalidation is his love language.