Web Development
Next.jsCachingPerformanceReactApp RouterSWR

Next.js Caching Strategies Explained: SSR, SSG, ISR, Route Cache, and SWR

AO
Adrijan Omićević
·15 min read

# What This Guide Covers#

Modern Next.js caching is not a single switch. In the App Router, you typically combine multiple layers: server rendering mode, server-side caches, CDN caching, and client-side caching.

This guide explains how Next.js caching strategies work in practice, when to use SSR, SSG, ISR, Route Cache, and SWR, and how to avoid expensive mistakes like serving stale auth or cross-tenant data.

You will leave with decision tables, production-ready snippets, and a mental model you can apply to landing pages, blogs, multi-tenant SaaS apps, and dashboards.

# The Modern Next.js Caching Model in App Router#

In App Router, caching is shaped by three main concerns:

  1. 1
    When HTML is produced: at request time, at build time, or on a revalidation schedule.
  2. 2
    Where results are stored: server output cache, server data cache, CDN, browser, and in-memory client caches.
  3. 3
    What makes a response dynamic: cookies, headers, search params, and explicitly dynamic config.

There are multiple caches in play. The names vary across docs and versions, but these concepts stay stable.

LayerWhat it cachesTypical winTypical risk
CDN / edge cacheFull responses for public pagesLowest latency globallyIncorrect caching of personalized pages
Route-level output cacheRendered output of a route segmentFast TTFB on repeat hitsStale HTML if misconfigured
Data cacheResults of fetch and related data callsLess backend loadCross-user or cross-tenant leaks if scoped wrong
Client cache (SWR)JSON responses and derived UI stateInstant transitions and fewer refetchesStale UI if invalidation is missing

ℹ️ Note: When teams say “Next.js is caching my page”, they often mean either cached render output or cached data fetches. These are not the same, and debugging gets much easier once you separate them.

If performance is your primary goal, also plan measurement and guardrails early. Pair caching work with real observability so you can see cache hit rates, revalidation frequency, and backend load before and after changes. A practical setup is covered in Web App Observability Guide: Logging, Metrics, Tracing.

# SSR, SSG, ISR: What They Mean in 2026 Next.js#

The old Pages Router terms still help, but in App Router you express them through caching directives and dynamic rendering behavior.

SSR: Server-Side Rendering per Request#

Use SSR when every request can legitimately produce a different result, like:

  • logged-in dashboards
  • tenant-specific admin screens
  • pricing shown in a user’s currency based on profile
  • A B tests that must be consistent per user

In App Router, SSR often means you opt out of caching by using no-store for data and ensuring the route is dynamic.

JavaScript
// app/dashboard/page.js
export const dynamic = 'force-dynamic';
 
export default async function DashboardPage() {
  const res = await fetch('https://api.example.com/me/summary', {
    cache: 'no-store',
    headers: { Authorization: `Bearer ${process.env.API_TOKEN}` },
  });
 
  const data = await res.json();
  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

When SSR matters: reducing data staleness is often more important than raw speed. If you are optimizing user-perceived performance for authenticated screens, focus on server streaming, reducing backend response times, and smart client caching for in-app navigation. For broader performance tuning tactics, see Website Performance Optimization.

SSG: Static Site Generation at Build Time#

Use SSG when content changes rarely and can be the same for everyone, like:

  • marketing pages
  • docs
  • blog posts
  • public product catalogs that update infrequently

In App Router, SSG typically happens when Next.js can determine a route has no dynamic usage and data is cacheable.

For known dynamic segments, you usually generate params at build time.

JavaScript
// app/blog/[slug]/page.js
export async function generateStaticParams() {
  const res = await fetch('https://cms.example.com/posts', { cache: 'force-cache' });
  const posts = await res.json();
  return posts.map((p) => ({ slug: p.slug }));
}
 
export default async function BlogPostPage({ params }) {
  const res = await fetch(`https://cms.example.com/posts/${params.slug}`, {
    cache: 'force-cache',
  });
  const post = await res.json();
  return <article>{post.title}</article>;
}

SSG matters when you want predictable, fast TTFB and minimal runtime infrastructure cost. A fully static route can be served from a CDN with excellent performance, often hitting sub-100 ms global TTFB depending on region and CDN.

ISR: Incremental Static Regeneration via Revalidation#

Use ISR when content is mostly static but must update without redeploying, like:

  • job listings
  • inventory availability
  • marketing pages with frequent copy changes
  • public dashboards that update every few minutes

In App Router, ISR is expressed with a revalidation window. Next.js caches the output and regenerates after the window elapses.

JavaScript
// app/jobs/page.js
export const revalidate = 300; // 5 minutes
 
export default async function JobsPage() {
  const res = await fetch('https://api.example.com/jobs', {
    next: { revalidate: 300 },
  });
  const jobs = await res.json();
  return <pre>{JSON.stringify(jobs, null, 2)}</pre>;
}

ISR matters because it reduces backend load dramatically while keeping content reasonably fresh. A 5-minute revalidate on a high-traffic jobs page can cut origin requests by more than 99 percent compared to SSR if most visitors hit the cached output.

⚠️ Warning: ISR is dangerous for anything user-specific. If the HTML includes data derived from cookies, session, tenant headers, or per-user entitlements, you can cache and serve the wrong content to the wrong user.

# Route Cache vs Data Cache: The Two Server-Side Pieces You Must Separate#

App Router encourages you to think in terms of route segments and server components. That’s powerful, but it can mask which part is actually cached.

Route Cache: Caching Rendered Output#

The Route Cache stores the rendered output for a route segment. If a segment is static or revalidated, the next visitor can get fast HTML without recomputing the entire tree.

This cache is especially valuable for pages where the React Server Component tree is large or expensive to render.

Data Cache: Caching Fetch Results#

The Data Cache stores results of server-side data requests, most commonly fetch. This reduces repeated calls to the same backend for identical requests.

In practice, you can have:

  • Cached route, cached data: best performance for public pages.
  • Dynamic route, cached data: common for semi-dynamic pages or shared reference data.
  • Cached route, dynamic data: possible if a small dynamic part forces re-rendering, but can still reuse cached subtrees depending on your structure.
  • Dynamic route, no cached data: correct for user-specific views.

A simple mental model:

What changes per user?Route output cacheData cacheTypical strategy
NothingYesYesSSG with force-cache
Changes occasionally for everyoneYes, with revalidateYes, with revalidateISR
Changes per request but same across usersUsually noSometimes yesSSR plus cached reference data
Changes per user or tenantNoNo for scoped endpointsSSR no-store plus SWR

# Decision Tables: Which Strategy to Use and When#

Most teams fail at caching because they pick a technique, not a policy. Decide based on freshness, personalization, and failure modes.

Page-Type Decision Table#

Page typePersonalizationFreshness needRecommended approachNotes
Marketing landing pageNoneLowSSGAdd CDN caching headers if applicable
Blog postNoneMediumISR revalidate 300 to 3600Add on-demand revalidate on publish
Public product listNone or minimalMediumISR plus tagged revalidateAvoid SSR if traffic is high
Logged-in dashboardHighHighSSR no-store plus SWRNever cache HTML across users
Multi-tenant adminHighHighSSR no-storeUse tenant-aware API and strict auth
Search resultsLow to mediumMediumSSR with short-lived caching or client fetchCaching depends on query patterns

Data-Type Decision Table#

DataExampleCacheabilityRecommended cachingWhy
Reference datacountries, plans, feature flagsHighData cache with long revalidateRarely changes, shared across users
Public contentblog post JSONHighData cache and ISRSame for everyone
Inventory countsstock levelMediumshort revalidate or on-demand tagsNeeds freshness but not per user
Auth/sessioncurrent user, permissionsNoneno-storeMust never leak
Tenant-scoped configbranding, limitsMedium but scopedcache per tenant key, or no-storeWrong cache key causes cross-tenant leaks

💡 Tip: If you cannot clearly define a safe cache key for a piece of data, treat it as uncacheable and use no-store. You can reintroduce caching later with explicit scoping.

# Practical Patterns and Code for App Router#

Pattern 1: Public Pages with SSG and Cached Data#

Goal: fastest possible TTFB and minimal backend load.

JavaScript
// app/page.js
export const dynamic = 'force-static';
 
export default async function HomePage() {
  const res = await fetch('https://cms.example.com/home', {
    cache: 'force-cache',
  });
  const home = await res.json();
  return <main>{home.heroTitle}</main>;
}

What to watch: if you accidentally use cookies or headers in this route, Next.js will treat it as dynamic and you will lose static caching.

Pattern 2: ISR for Content That Changes Without Deploys#

Goal: keep pages fast while avoiding stale content for too long.

JavaScript
// app/pricing/page.js
export const revalidate = 600;
 
export default async function PricingPage() {
  const res = await fetch('https://cms.example.com/pricing', {
    next: { revalidate: 600 },
  });
  const pricing = await res.json();
  return <pre>{JSON.stringify(pricing, null, 2)}</pre>;
}

When 10 minutes is not acceptable, use on-demand revalidation with tags, described below.

Pattern 3: On-Demand Revalidation with Tags#

Time-based revalidate is blunt. Tags let you invalidate only what changed.

Use tags on fetch:

JavaScript
// app/lib/cms.js
export async function getPost(slug) {
  const res = await fetch(`https://cms.example.com/posts/${slug}`, {
    next: { tags: ['post', `post:${slug}`] },
  });
  return res.json();
}

Then trigger revalidation from a webhook endpoint:

JavaScript
// app/api/revalidate/route.js
import { revalidateTag } from 'next/cache';
 
export async function POST(request) {
  const body = await request.json();
  const slug = body.slug;
 
  revalidateTag('post');
  revalidateTag(`post:${slug}`);
 
  return Response.json({ ok: true });
}

This pattern matters for editorial workflows. If your CMS publishes 50 posts per day, tag-based revalidation avoids regenerating unrelated pages.

Pattern 4: Authenticated SSR Without Stale User Data#

Goal: correctness and security first, then speed.

Common requirements:

  • user profile must reflect latest permissions
  • tenant context must be correct
  • no cross-user HTML caching
JavaScript
// app/account/page.js
import { cookies } from 'next/headers';
 
export const dynamic = 'force-dynamic';
 
export default async function AccountPage() {
  const token = cookies().get('session')?.value;
 
  const res = await fetch('https://api.example.com/me', {
    cache: 'no-store',
    headers: { Authorization: `Bearer ${token}` },
  });
 
  if (!res.ok) throw new Error('Failed to load profile');
  const me = await res.json();
 
  return <main>Signed in as {me.email}</main>;
}

This is where React Server Components can help: you can keep private data on the server, reduce client bundle size, and still stream UI quickly. If you need a refresher on how the server and client component boundary affects data flow, see React Server Components Guide.

# SWR: Client-Side Stale-While-Revalidate That Complements Server Caching#

Server caching improves TTFB and reduces origin load. SWR improves in-app perceived performance, especially after the initial page load.

Use SWR when:

  • users navigate between tabs in a dashboard
  • you want cached UI immediately, then revalidate
  • you need optimistic updates for forms and toggles
  • you need periodic refresh while staying on the same screen

Minimal SWR Setup#

JavaScript
// app/lib/fetcher.js
export async function fetcher(url) {
  const res = await fetch(url, { credentials: 'include' });
  if (!res.ok) throw new Error('Request failed');
  return res.json();
}
JavaScript
// app/dashboard/components/KpiCard.client.js
'use client';
 
import useSWR from 'swr';
import { fetcher } from '@/app/lib/fetcher';
 
export function KpiCard() {
  const { data, isLoading, error } = useSWR('/api/kpi', fetcher, {
    revalidateOnFocus: true,
    dedupingInterval: 10_000,
  });
 
  if (isLoading) return 'Loading...';
  if (error) return 'Failed to load';
  return `Revenue: ${data.revenue}`;
}

Why it matters: deduping means multiple components can request the same key and SWR will avoid duplicate network calls within the interval. For dashboards with 10 KPI cards, this can reduce client chatter by 50 to 90 percent depending on how you structure keys.

SWR Invalidation After Mutations#

Most stale UI problems are not caused by caching. They are caused by missing invalidation after changes.

JavaScript
// app/dashboard/components/Toggle.client.js
'use client';
 
import useSWR, { mutate } from 'swr';
import { fetcher } from '@/app/lib/fetcher';
 
export function Toggle() {
  const { data } = useSWR('/api/settings', fetcher);
 
  async function onToggle() {
    await fetch('/api/settings', { method: 'POST' });
    await mutate('/api/settings');
  }
 
  return <button onClick={onToggle}>Refresh settings</button>;
}

This pattern matters when correctness is important but you still want a snappy UI.

# Common Pitfalls in Next.js Caching Strategies#

These are the issues we see most often in audits and production incidents.

Pitfall 1: Caching Authenticated HTML#

If a route reads cookies, headers, or session state and still ends up cached, you can leak user data. This can happen through misapplied revalidate settings or assuming a hosting platform will “do the right thing”.

Mitigation:

  • mark user-specific routes as dynamic
  • use cache: 'no-store' for user data
  • ensure CDNs do not cache authenticated responses

Pitfall 2: Cross-Tenant Data Leaks from Shared Data Cache#

Multi-tenant apps often pass tenant context via:

  • subdomain
  • X-Tenant-Id header
  • cookie
  • JWT claim

If your cached fetch does not vary by that tenant context, one tenant can receive another tenant’s data.

Mitigation:

  • include tenant identifier in the request URL or request headers consistently
  • treat tenant-scoped endpoints as no-store unless you can guarantee correct cache keys
  • separate public and private data paths

🎯 Key Takeaway: In multi-tenant systems, “cacheable” is not a property of the endpoint. It is a property of the endpoint plus the full set of inputs that affect the response, especially tenant and auth context.

Pitfall 3: Assuming Revalidate Means “Always Fresh”#

Revalidation is not a guarantee of immediate freshness. It is a policy that trades freshness for performance.

If your business requirement is “must update within 10 seconds”, a 5-minute revalidate is not a solution. Use on-demand revalidation or SSR for that portion of the page.

Pitfall 4: Mixing Search Params with Static Caching#

Search pages with query strings can explode your cache cardinality. If you cache every unique query, you may end up with low hit rates and high storage churn.

Mitigation:

  • SSR search results
  • cache only popular queries
  • move search to client fetch with SWR and server-side rate limiting

Pitfall 5: Debugging Without Metrics#

Caching failures look like random staleness until you can see:

  • cache hit or miss by route
  • backend request count per page view
  • revalidation events
  • response age

If you do not measure, you will either over-cache and break correctness or under-cache and miss performance gains. Pair performance work with Website Performance Optimization and instrumentation from Web App Observability Guide: Logging, Metrics, Tracing.

# A Practical “Which One Should I Use?” Checklist#

Use this checklist before you implement a caching policy.

Step 1: Classify the page#

  1. 1
    Is any part user-specific or tenant-specific
  2. 2
    Does it need to reflect changes within minutes or seconds
  3. 3
    What is the traffic profile: long-tail or concentrated
  4. 4
    What breaks if content is stale

Step 2: Choose the baseline rendering mode#

  • If user-specific: SSR and no-store
  • If public and stable: SSG
  • If public and changing: ISR with revalidate and possibly tags

Step 3: Decide how to handle interactivity#

  • If content is interactive and changes in-session: SWR with mutation invalidation
  • If content is mostly read-only: rely more on server caching

Step 4: Add safe invalidation paths#

  • Time-based revalidate for simple cases
  • Tag-based revalidation for CMS and structured content
  • Explicit SWR mutate calls after updates

# Key Takeaways#

  • Treat caching as layered: route output cache, data cache, CDN cache, and client SWR all solve different problems.
  • Use SSG for truly public and stable pages, ISR for public pages that change without redeploys, and SSR no-store for auth- or tenant-scoped pages.
  • Separate Route Cache and Data Cache in your mental model so you can reason about what is actually being reused.
  • Prefer tag-based on-demand revalidation when you need fast freshness after CMS updates without regenerating everything.
  • For dashboards, combine SSR correctness with SWR for instant client-side transitions and explicit invalidation after mutations.

# Conclusion#

Next.js caching strategies are most effective when you start from correctness and data scope, then add caching only where you can define safe keys and invalidation rules. If you want help designing a caching policy for a multi-tenant SaaS, auditing staleness risks, or improving performance without breaking auth flows, Samioda can help.

Contact us to review your Next.js App Router setup and ship a caching strategy that is fast, fresh, and safe.

FAQ

Share
A
Adrijan OmićevićSamioda Team
All articles →

Need help with your project?

We build custom solutions using the technologies discussed in this article. Senior team, fixed prices.