Implement SSR caching in Next.js by setting Cache-Control headers with s-maxage and stale-while-revalidate directives in getServerSideProps, enabling CDN caching with background revalidation
Implementing caching for Server-Side Rendering (SSR) in Next.js transforms dynamic pages into performant, cacheable assets. By setting appropriate Cache-Control headers in getServerSideProps, you instruct CDNs to store rendered HTML and serve it instantly to users, while using stale-while-revalidate to keep content fresh in the background. This approach reduces server load, improves Time to First Byte (TTFB), and provides a balance between dynamic data needs and static performance.
public: Allows any cache (CDN, proxy, browser) to store the response. Essential for CDN caching.
s-maxage: Specifies how long the CDN should consider the response fresh (in seconds). After this, it becomes stale.
stale-while-revalidate: During this window, the CDN can serve the stale response while asynchronously fetching a fresh version.
stale-if-error: If the origin fails during revalidation, serve stale content for up to this many seconds as a fallback.
private: Prevents CDN caching; only browser can cache. Use for user-specific content.
no-cache, no-store: Disables caching entirely. Use for sensitive or real-time data.
The stale-while-revalidate pattern is the foundation of modern SSR caching. When a request arrives for a cached page, the CDN checks its freshness: If within s-maxage, the cached version is served instantly (HIT). If past s-maxage but within stale-while-revalidate window, the CDN serves the stale version immediately while triggering a background request to your origin to generate a fresh copy. The next user gets the updated version. This ensures users never wait for page generation, even during cache expiration, while content eventually updates. This pattern is identical to how ISR works, but applied to SSR responses.
When deploying to platforms like Vercel, the Cache-Control headers are automatically respected by their global CDN. You can monitor cache performance using response headers: x-vercel-cache indicates HIT, MISS, or STALE. For self-hosted deployments with CDNs like Cloudflare, Fastly, or AWS CloudFront, these headers are also respected. However, you may need to configure your CDN to forward the Cache-Control headers correctly and set up purging mechanisms for manual invalidation when content changes unexpectedly.
Marketing pages (homepage, about): s-maxage=3600, stale-while-revalidate=86400 (1 hour fresh, 24 hours stale) - content rarely changes
Product pages (e-commerce): s-maxage=300, stale-while-revalidate=3600 (5 minutes fresh, 1 hour stale) - price/inventory may update
Blog posts: s-maxage=3600, stale-while-revalidate=86400 - updates infrequent, but comments can be client-loaded
API endpoints: s-maxage=60, stale-while-revalidate=300 - balance freshness and performance
User dashboards: private, no-cache - never cache user-specific data
Error pages (404, 500): s-maxage=5, stale-while-revalidate=60 - brief cache to prevent stampedes
When you serve different content based on request characteristics (geolocation, device type, language), you need the Vary header. It tells the CDN to cache multiple versions of the same URL, keyed by specific request headers. For example, if you serve different content for mobile vs desktop, use Vary: User-Agent. If content differs by country (using Vercel's geolocation headers), use Vary: X-Vercel-IP-Country. This ensures users get the right version without hitting your origin.
In the App Router, the pattern changes slightly. For API routes, you set headers on the NextResponse object as shown in the example. For page components, you typically use Server Components with Suspense and streaming, but you can still set headers using the headers() function in a layout or page.
Route Handlers (API routes): Use NextResponse.json() or NextResponse.next() with headers option.
Server Components: You can't directly set headers from a Server Component, but you can use middleware to set cache headers based on the route.
Middleware approach: Set Cache-Control headers in middleware for specific paths, which is often cleaner than setting them in every getServerSideProps.
The stale-while-revalidate pattern works the same way regardless of router version.
With aggressive caching, you need a way to invalidate content when it changes. For time-sensitive updates, use on-demand revalidation via webhooks (similar to ISR). For immediate updates, you can implement a purge mechanism with your CDN. Vercel provides the revalidatePath and revalidateTag APIs that work with cached SSR responses. When you call these functions, the CDN cache is purged and the next request triggers a fresh render. This gives you the performance of caching with the flexibility of instant updates when content changes.