How to Fix: SSR and Cache Control Issues with Next.js: Help Needed
SSR cache bugs in Next.js usually come from one of three places: the page is being statically optimized when you expected per-request rendering, upstream data is cached more aggressively than intended, or HTTP Cache-Control headers are missing or set at the wrong layer. The result is stale HTML, stale API data, or pages that appear server-rendered but behave like cached snapshots.
Understanding the Root Cause
In Next.js, server-side rendering does not automatically mean no caching. That is the main source of confusion.
Depending on whether your app uses the Pages Router or the App Router, Next.js may cache at several levels:
- HTML caching at the framework or hosting layer
- fetch() response caching in server components or route handlers
- CDN caching controlled by response headers
- Browser caching controlled by standard HTTP headers
Typical failure modes include:
- A page expected to be dynamic is actually being statically generated.
- A server component uses
fetch()without disabling caching, so old data is reused. getServerSidePropsruns, but the response headers still allow a reverse proxy or CDN to cache the HTML too long.- A custom API route returns cacheable data, causing SSR pages that depend on it to appear stale.
- Hosting providers add another caching layer unless headers are explicit.
For the Pages Router, pages using getServerSideProps are rendered on every request, but the final HTTP response can still be cached unless you set the correct headers. For the App Router, rendering can look dynamic while individual fetch() calls remain cached unless you use options like cache: 'no-store' or route-level dynamic settings.
So the real issue is not just SSR. It is the interaction between rendering mode and data cache policy.
Step-by-Step Solution
The safest fix is to make the page explicitly dynamic, disable unintended data caching, and send clear HTTP cache headers.
1. Confirm which router your project uses
If your code uses pages/, you are on the Pages Router. If it uses app/, you are on the App Router. The fix differs slightly.
2. Pages Router: use getServerSideProps and set Cache-Control explicitly
If the page must always render fresh data per request, use getServerSideProps and set response headers there.
export async function getServerSideProps({ res }) {
res.setHeader(
'Cache-Control',
'no-store, no-cache, must-revalidate, proxy-revalidate'
)
const response = await fetch('https://example.com/api/data')
const data = await response.json()
return {
props: { data },
}
}
export default function Page({ data }) {
return <div>{data.title}</div>
}
If you want controlled CDN caching instead of fully disabling it, use stale-while-revalidate:
export async function getServerSideProps({ res }) {
res.setHeader(
'Cache-Control',
'public, s-maxage=60, stale-while-revalidate=300'
)
const response = await fetch('https://example.com/api/data')
const data = await response.json()
return {
props: { data },
}
}
This tells shared caches to keep content fresh for 60 seconds and serve stale content while background revalidation happens for up to 300 seconds.
3. App Router: disable fetch caching for truly dynamic data
In the App Router, a page can still serve cached data unless you opt out.
async function getData() {
const res = await fetch('https://example.com/api/data', {
cache: 'no-store',
})
if (!res.ok) {
throw new Error('Failed to fetch data')
}
return res.json()
}
export default async function Page() {
const data = await getData()
return <div>{data.title}</div>
}
You can also force the whole route to be dynamic:
export const dynamic = 'force-dynamic'
async function getData() {
const res = await fetch('https://example.com/api/data', {
cache: 'no-store',
})
return res.json()
}
export default async function Page() {
const data = await getData()
return <div>{data.title}</div>
}
If you want periodic revalidation instead of no caching:
export const revalidate = 60
async function getData() {
const res = await fetch('https://example.com/api/data', {
next: { revalidate: 60 },
})
return res.json()
}
4. Fix API routes that return stale data
If your SSR page calls your own API route, that route may be the real cache problem. Add explicit headers there too.
export default async function handler(req, res) {
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate')
const data = {
message: 'fresh data',
time: Date.now(),
}
res.status(200).json(data)
}
For App Router route handlers:
export async function GET() {
const data = {
message: 'fresh data',
time: Date.now(),
}
return new Response(JSON.stringify(data), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
},
})
}
5. Avoid calling your own API from SSR when direct server access is possible
A common performance and caching mistake is this flow:
- SSR page calls internal API route
- API route calls database or backend service
- Cache behavior becomes harder to reason about
If the page is already running on the server, fetch directly from the database or backend service when possible.
export async function getServerSideProps({ res }) {
res.setHeader('Cache-Control', 'no-store')
const data = await getDataFromDatabase()
return {
props: { data },
}
}
This removes one layer of caching and one extra network hop.
6. Check hosting behavior
If deployed on Vercel or behind a reverse proxy, inspect the response headers in the browser network tab or with a terminal request. You should verify what is actually returned in production, not just in local development.
curl -I https://your-site.example/page
Look for headers such as:
Cache-Controlx-vercel-cacheageetag
If the page should never be cached, Cache-Control should clearly reflect that.
7. Separate cache strategy by content type
Not everything should use no-store. A better production strategy is:
- User-specific dashboards:
no-store - Fast-changing admin pages:
no-storeor very low TTL - Public content updated every few minutes:
s-maxageplusstale-while-revalidate - Rarely changing marketing content: static generation or ISR
The bug often happens because one cache strategy is applied to all routes.
Common Edge Cases
Using getServerSideProps but still seeing stale content
This usually means a proxy, CDN, or hosting layer is caching the HTML response. Set explicit response headers and verify them in production.
App Router page updates only after hard refresh
Your page may be dynamic, but a nested fetch() call is still cached. Add cache: 'no-store' or next: { revalidate: 0 } to the request that serves changing data.
Data differs between development and production
In development, caching behavior is often less aggressive or bypassed. Production enables optimizations that expose hidden cache assumptions.
Authenticated pages leak shared cached content
Never use public shared caching for user-specific responses. For authenticated pages, prefer private or no-store policies.
Route handlers return fresh JSON, but page HTML is stale
The API route may be correct while the outer page response is cached. Fix both layers independently.
Revalidation does not happen when expected
Using ISR or revalidation settings can still produce delays if upstream APIs, edge caches, or tag-based invalidation are not configured consistently.
Browser cache masks server changes
If HTML looks fresh on the server but not in the browser, inspect request headers and disable browser cache temporarily in devtools while debugging.
FAQ
Should I always use no-store for SSR pages?
No. Use no-store for highly dynamic or user-specific content. For public pages, controlled caching with s-maxage and stale-while-revalidate is usually faster and more scalable.
What is the difference between force-dynamic and cache: ‘no-store’ in Next.js?
force-dynamic affects the route rendering mode, while cache: 'no-store' affects an individual fetch() request. In many real-world bugs, you need both the route and the data fetch behavior to be explicit.
Why does my SSR page call the server every time locally but not in production?
Because production deployments often introduce framework-level and CDN-level caching. Local development is not a reliable indicator of final cache behavior.
The practical fix is simple: identify whether the stale output comes from page rendering, data fetching, or HTTP caching, then make each layer explicit. In Next.js, caching is powerful, but when it is left implicit, SSR bugs are almost guaranteed.