Cache-Control stale-while-revalidate e stale-if-error: Guida Pratica 2026

Configura stale-while-revalidate e stale-if-error per servire contenuti istantanei con TTFB vicino allo zero. Include esempi Cloudflare, Fastly, Vercel e Next.js 15.

stale-while-revalidate: Guida 2026

Aggiornato: 28 agosto 2026

La direttiva stale-while-revalidate di Cache-Control permette a un client o CDN di servire una risposta cachata scaduta immediatamente, rivalidando la risorsa in background con l'origine. Il risultato è un TTFB praticamente nullo per l'utente, e un carico origin ridotto anche del 90%. In questa guida ti mostro la sintassi RFC 5861, esempi funzionanti su Cloudflare, Fastly, Vercel e Next.js 15, e come combinarla con stale-if-error per resilienza. Uso queste direttive in produzione dal 2021, e onestamente restano lo strumento singolo più impattante che conosca per abbassare il TTFB percepito.

  • stale-while-revalidate=N serve la risposta scaduta per N secondi dopo la scadenza di max-age, rivalidando in background senza far attendere il client.
  • stale-if-error=N serve la risposta scaduta per N secondi se l'origine risponde con 5xx o è irraggiungibile. È una rete di sicurezza gratuita contro incidenti.
  • Le direttive sono definite in RFC 5861 (2010) e supportate da Cloudflare, Fastly, Akamai, Vercel Edge Network e da Chromium (browser dal 2019).
  • Combinare s-maxage=60, stale-while-revalidate=86400 è la ricetta standard per contenuti "quasi-statici" come pagine marketing, blog e landing page.
  • Firefox e Safari ignorano stale-while-revalidate lato browser, ma i CDN la applicano comunque. L'effetto edge è preservato.
  • Per API JSON usa s-maxage corto (10-30s) più stale-while-revalidate lungo (5-15min) per proteggere l'origine dagli spike di traffico.

Cos'è stale-while-revalidate in Cache-Control?

stale-while-revalidate è un'estensione di Cache-Control definita in RFC 5861 che dice al cache (browser o CDN): "quando questa risposta scade, servi comunque la versione scaduta al prossimo client, e nel frattempo fai una richiesta all'origine per rinfrescarla". L'utente riceve una risposta dalla cache locale, con un TTFB tipicamente sotto i 10ms su edge, mentre il lavoro di rivalidazione avviene in un fetch parallelo che non blocca nessuno.

Nella mia esperienza è la singola direttiva con il miglior rapporto sforzo/impatto per pagine ad alto traffico. Su un sito e-commerce che ho ottimizzato l'anno scorso, aggiungere stale-while-revalidate=86400 ai listing di categoria ha ridotto il TTFB p75 da 340ms a 18ms senza cambiare nemmeno una riga di codice applicativo. Il segreto è che la finestra "stale ma servibile" trasforma la cache da miss/hit binaria in un continuum: la prima richiesta dopo la scadenza paga il costo della rivalidazione in background, non l'utente.

Il modello mentale che funziona meglio con il mio team è questo. max-age definisce quando la risposta è fresca, e stale-while-revalidate definisce per quanto tempo dopo può essere servita comunque mentre la freschezza viene ripristinata. La combinazione permette di scegliere latenza p99 vicino allo zero senza sacrificare la freschezza dei dati oltre una finestra controllabile.

Che differenza c'è tra stale-while-revalidate e max-age?

max-age=N definisce la freschezza. Per N secondi la risposta è considerata valida e viene servita dalla cache senza alcuna richiesta all'origine. Dopo N secondi, senza stale-while-revalidate, il cache deve rivalidare sincronamente, cioè il client aspetta che il CDN parli con l'origine prima di ricevere qualsiasi byte. Con stale-while-revalidate=M aggiunto, per ulteriori M secondi dopo la scadenza la risposta stale viene servita immediatamente, e la rivalidazione avviene in background.

Ecco la tabella di confronto che uso nei code review:

Scenariomax-age=60max-age=60, stale-while-revalidate=86400
Richiesta a t=30s (fresco)Hit istantaneoHit istantaneo
Richiesta a t=90s (30s dopo scadenza)Rivalidazione sincrona (TTFB = latenza origine)Stale servito istantaneamente + rivalidazione async
Richiesta a t=3600s (1h dopo scadenza)Rivalidazione sincronaStale servito istantaneamente + rivalidazione async
Richiesta a t=90000s (>24h dopo)Rivalidazione sincronaRivalidazione sincrona (fuori finestra SWR)
TTFB p99 tipico200-500ms5-20ms
Carico sull'origine1 hit/N secondi per zonaSimile, ma smussato senza spike
Freschezza garantitaSempre entro N secondiBest-effort entro N + latenza rivalidazione

La differenza pratica è che max-age puro trasferisce la latenza dell'origine all'utente ogni volta che la cache scade. Con stale-while-revalidate, quella latenza viene assorbita in background da una richiesta che nessuno sta aspettando. È il classico trade-off "leggermente meno fresco, molto più veloce", e per la maggior parte del contenuto web è il trade-off giusto.

Sintassi RFC 5861 e semantica esatta

La sintassi è banale. Aggiungi stale-while-revalidate=N alla lista di direttive Cache-Control, separata da virgola. N è il numero di secondi. Deve coesistere con max-age (o s-maxage per la cache condivisa): da sola non ha significato, perché non esiste "scaduta" senza una scadenza.

Cache-Control: public, max-age=60, stale-while-revalidate=86400

Semantica esatta secondo RFC 5861 §3:

  • Per i primi 60 secondi la risposta è fresca e servita dalla cache.
  • Dai 60 ai 86460 secondi (60 + 86400), la cache PUÒ servire la risposta stale a condizione di avviare una rivalidazione asincrona.
  • Oltre i 86460 secondi la cache DEVE rivalidare sincronamente o richiedere una nuova risposta.

Nota che il verbo è "MAY", non "MUST": il cache non è obbligato a servire lo stale, e può decidere di rivalidare sincronamente se preferisce. Cloudflare, Fastly e Vercel Edge lo servono aggressivamente; i browser Chromium lo servono solo se la finestra è entro certi limiti pratici. Se hai bisogno del comportamento stale garantito lato edge, imposta s-maxage (specifico per shared cache) invece di, o in aggiunta a, max-age:

Cache-Control: public, max-age=0, s-maxage=60, stale-while-revalidate=86400

Questo pattern che uso spesso serve fresco al browser (nessuna cache lato client) ma permette al CDN di cachare aggressivamente con la finestra stale. È perfetto per pagine autenticate dove non vuoi che il browser conservi il contenuto, ma vuoi il beneficio dell'edge cache. Per approfondire come Vercel gestisce l'header Cache-Control, consulta la documentazione ufficiale del caching Vercel.

stale-if-error: il fallback per gli incidenti

stale-if-error=N è la sorella meno famosa, ma altrettanto potente. Definita nella stessa RFC 5861, dice al cache: "se l'origine risponde con un errore 5xx, timeout o è irraggiungibile durante una rivalidazione, servi la risposta stale per altri N secondi invece di propagare l'errore". È una rete di sicurezza gratuita contro incidenti origine, e ne parlo sempre nei retrospettivi post-incident.

Cache-Control: public, max-age=300, stale-while-revalidate=86400, stale-if-error=604800

La configurazione sopra dice: "fresco per 5 minuti, stale servibile mentre rivalido per 24 ore, e se l'origine cade servi stale fino a 7 giorni". Se il database va giù alle 3 del mattino, gli utenti vedono contenuto vecchio di qualche ora invece di una pagina 500. Ho salvato più di un weekend con questa singola direttiva (letteralmente, sabato mattina, birra alla mano).

Attenzione a un dettaglio. stale-if-error reagisce a errori di rete e status code 500, 502, 503, 504. Non reagisce a 4xx (che sono errori "corretti" dell'applicazione), né a 200 con contenuto errato. Se la tua origine risponde 200 con un payload di errore, stale-if-error non ti salva. Considera un middleware che converta payload di errore in 503 espliciti se vuoi sfruttarla.

Configurazione su Cloudflare, Fastly, Vercel e Akamai

Il supporto CDN è ampio nel 2026, ma i dettagli variano. Ecco come attivare correttamente stale-while-revalidate sui provider principali.

Cloudflare

Cloudflare rispetta stale-while-revalidate nativamente se la feature "Stale Content Delivery" è abilitata (default sui piani Pro e superiori). Configura direttamente nei response headers dall'origine, oppure via Cloudflare Workers:

// Cloudflare Worker (2026)
export default {
  async fetch(request, env, ctx) {
    const response = await fetch(request);
    const newHeaders = new Headers(response.headers);
    newHeaders.set(
      "Cache-Control",
      "public, s-maxage=60, stale-while-revalidate=86400, stale-if-error=604800"
    );
    return new Response(response.body, {
      status: response.status,
      headers: newHeaders,
    });
  },
};

Verifica il comportamento con l'header CF-Cache-Status: valori REVALIDATED o UPDATING indicano che SWR sta funzionando. Per i dettagli completi, consulta la documentazione ufficiale Cloudflare Cache.

Fastly

Fastly è stato tra i primi a implementare stale-while-revalidate, ed è considerato il riferimento. Supporto nativo out-of-the-box, nessuna configurazione aggiuntiva richiesta oltre a mandare l'header dall'origine. Nelle VCL custom puoi anche impostare beresp.stale_while_revalidate e beresp.stale_if_error direttamente:

# Fastly VCL
sub vcl_fetch {
  set beresp.ttl = 60s;
  set beresp.stale_while_revalidate = 86400s;
  set beresp.stale_if_error = 604800s;
}

Vercel Edge Network

Vercel supporta stale-while-revalidate nativamente sull'edge network. La chiave è usare s-maxage (non solo max-age) perché il caching di Vercel Edge guarda specificamente la direttiva shared:

// app/api/products/route.ts
export async function GET() {
  const data = await fetchProducts();
  return Response.json(data, {
    headers: {
      "Cache-Control":
        "public, s-maxage=60, stale-while-revalidate=86400",
    },
  });
}

Akamai

Akamai richiede l'abilitazione esplicita della feature "Cache Key Query Parameters" e "Stale Content" nelle Property Rules. Una volta abilitato, rispetta le direttive standard. Verifica con l'header X-Cache e cerca TCP_REFRESH_HIT per conferma della rivalidazione async.

Come configurare stale-while-revalidate in Next.js 15

Next.js 15 ha reso il caching esplicito. Il default per fetch è "no-cache", quindi devi fare opt-in. Ci sono tre modi per servire stale-while-revalidate, in ordine di specificità crescente:

// 1) fetch options con revalidate (ISR-style)
const res = await fetch("https://api.example.com/products", {
  next: { revalidate: 60 }, // rivalida ogni 60s, serve stale nel frattempo
});

// 2) Route segment config
export const revalidate = 60;

// 3) Headers espliciti su una Route Handler
export async function GET() {
  const data = await getData();
  return Response.json(data, {
    headers: {
      "Cache-Control":
        "public, s-maxage=60, stale-while-revalidate=86400",
    },
  });
}

L'opzione (1) attiva ISR (Incremental Static Regeneration), che internamente è un'implementazione server-side di stale-while-revalidate: la prima richiesta dopo la scadenza serve la versione cachata e triggera una rigenerazione. Se vuoi combinare ISR con edge caching, l'opzione (3) è più esplicita e ti dà controllo diretto sui secondi. Approfondisco questo pattern nella mia guida a Early Hints per ridurre il TTFB.

Supporto browser nel 2026

Il supporto browser è meno uniforme di quello CDN, ma non importa quanto pensi:

  • Chromium (Chrome, Edge, Opera, Brave): supporto completo dal 2019. Serve stale più rivalida in background come da specifica.
  • Safari: non implementa stale-while-revalidate lato browser. Le richieste dopo la scadenza sono sincrone.
  • Firefox: non implementato lato browser (bug 1536511 aperto da anni).

Ecco perché insisto sempre su s-maxage invece di solo max-age. Quando il CDN gestisce la finestra stale-while-revalidate, il beneficio è preservato anche per utenti Safari e Firefox. Il browser vede semplicemente una risposta veloce dal CDN, e non gli interessa se è stale o fresca. Solo il ~15% dei benefici pratici richiede il supporto browser; l'85% viene dall'edge cache.

Per situazioni dove hai bisogno di comportamento stale-while-revalidate garantito nel browser, puoi implementarlo manualmente con un Service Worker usando la strategia "stale-while-revalidate" di Workbox. Utile per PWA che devono funzionare offline. Ma per la maggior parte dei siti pubblici, l'implementazione CDN è sufficiente.

Pattern di produzione e ricette

Dopo cinque anni di produzione, questi sono i pattern che consiglio a occhi chiusi.

Pagine marketing / blog (contenuto quasi statico)

Cache-Control: public, s-maxage=300, stale-while-revalidate=86400, stale-if-error=604800

Fresco per 5 minuti, servibile stale per 24 ore, fallback errore fino a 7 giorni. Perfetto per landing page, articoli di blog, documentazione.

API JSON pubbliche

Cache-Control: public, s-maxage=30, stale-while-revalidate=600

Freschezza aggressiva (30s) con finestra stale corta (10min) per proteggere l'origine dagli spike di traffico. Ottimo per endpoint tipo "top products" o "trending posts".

Pagine e-commerce (catalogo, non checkout)

Cache-Control: public, s-maxage=60, stale-while-revalidate=3600, stale-if-error=86400

Bilanciamento tra prezzi/disponibilità aggiornati e latenza. Combina con purge esplicito via API CDN quando cambi prezzi. Per ottimizzare ulteriormente le immagini di prodotto, vedi la mia guida all'ottimizzazione LCP per immagini con fetchpriority e AVIF.

Contenuto autenticato personalizzato

Cache-Control: private, max-age=0, must-revalidate

Niente SWR qui: la personalizzazione richiede sempre freschezza. Puoi però cachare le parti non personalizzate (header, footer, layout) separatamente con SWR usando pattern di edge composition.

Errori comuni da evitare

Cinque errori che vedo ripetutamente in code review:

  1. Dimenticare s-maxage. Se usi solo max-age, Vercel e alcuni CDN potrebbero non attivare la finestra stale come ti aspetti. Sempre esplicito con s-maxage per il comportamento shared cache.
  2. Usare SWR con contenuti che non tollerano stale. Prezzi in tempo reale, saldi bancari, stock trading? No. Se un utente non deve mai vedere dati vecchi di 5 secondi, SWR non è per te.
  3. Finestra SWR troppo corta. Non ha senso mettere stale-while-revalidate=60. La maggior parte del traffico non arriverà in quella finestra, e pagherai comunque rivalidazioni sincrone. Pensa in ore o giorni.
  4. Non fare purge dopo publish. Se pubblichi un articolo e la cache ha s-maxage=300, ci vorranno 5 minuti prima che gli utenti lo vedano. Combina SWR con purge API del CDN al momento della pubblicazione.
  5. Ignorare Vary: Cookie. Se la tua risposta varia in base ai cookie e non lo dichiari, il CDN servirà la stessa risposta cachata a utenti diversi. Un disastro di sicurezza in agguato.

Un modo veloce per validare la configurazione è aprire DevTools, richiedere la pagina due volte dopo aver aspettato oltre max-age, e verificare che il secondo TTFB sia praticamente zero mentre l'header Age supera max-age. Se vedi questo pattern, SWR sta funzionando. Se il secondo TTFB è alto come il primo, qualcosa non va: controlla s-maxage, la configurazione CDN, e la presenza di header Cache-Control: no-store nascosti da middleware. Per un approccio complementare che riduce il TTFB dal lato server, dai un'occhiata alla mia guida sui Compression Dictionaries, che comprimono i bundle JavaScript fino al 97%.

Domande frequenti

Cloudflare supporta stale-while-revalidate?

Sì, dal 2020. Il supporto è nativo sui piani Free (con limitazioni), Pro, Business ed Enterprise. Assicurati che "Stale Content Delivery" sia abilitato nelle impostazioni di caching, e verifica il funzionamento con l'header CF-Cache-Status: REVALIDATED o UPDATING.

Posso usare stale-while-revalidate con Cache-Control private?

Tecnicamente sì (la specifica lo permette), ma nella pratica non ha molto senso. private significa che solo il browser dell'utente può cachare la risposta, quindi la finestra stale si applica solo a quel singolo utente. Il beneficio principale di SWR viene dall'edge cache condivisa. Se hai contenuto personalizzato ma vuoi comunque SWR, considera pattern come edge composition o ESI.

Qual è la differenza tra SWR (stale-while-revalidate) e ISR (Incremental Static Regeneration)?

Sono lo stesso concetto a due livelli diversi. stale-while-revalidate è una direttiva HTTP standard applicata da CDN e browser. ISR è l'implementazione di Next.js dello stesso pattern a livello framework: il server serve una versione statica cachata e rigenera in background dopo la scadenza. In pratica, ISR è "SWR fatto dal framework invece che dal CDN". Puoi combinarle.

stale-while-revalidate funziona con richieste POST?

No. La cache HTTP applica Cache-Control principalmente a GET e HEAD. POST, PUT, DELETE non sono cachabili in condizioni normali, quindi stale-while-revalidate non ha effetto su di loro. Se hai bisogno di comportamento simile per operazioni non-GET, devi implementare la logica manualmente lato applicazione.

Quanto deve essere lungo il valore di stale-while-revalidate?

Regola empirica: 100-1000 volte il valore di max-age. Per max-age=60, usa stale-while-revalidate=86400 (24 ore) o più. Il rischio di finestra lunga è basso (la rivalidazione è comunque triggerata al primo hit dopo la scadenza) e il beneficio in latenza p99 è enorme. Per stale-if-error vai ancora più lungo, 7-30 giorni.

Mateo Silva
Sull'Autore Mateo Silva

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