ISR Stale Data Fix: Cache Keys, Headers, and Edge TTL
Paying users saw expired pricing on our academy portal. Here is the exact Next.js ISR failure and the cache key fix we shipped.
Author
The Incident: Paying Users Saw Expired Subscription Pricing on the Academy Portal
In March 2026, our client-facing academy portal started serving stale subscription pricing to paying users. A user upgraded their plan, but the checkout page still showed the old price for over an hour. The root cause was a misconfigured ISR cache key combined with an edge CDN override.
We confirmed the problem with a single curl command:
curl -sI https://academy.example.com/pricing
# x-vercel-cache: HIT, age: 3500, cache-control: public, s-maxage=3600
The age header was 3500 seconds, far past our intended revalidate: 60. The cache-control header was public, s-maxage=3600, which meant the edge CDN was holding the page for a full hour regardless of ISR.
What We Tried First (And Why It Failed)
We initially tried calling revalidatePath('/pricing') from the admin webhook after a price update. It did not work because the route was being cached at the Cloudflare edge layer with an Edge TTL of 3600 seconds, which bypassed Vercel's ISR entirely Next.js ISR Revalidation Stuck Serving Stale Pages.
We also tried setting revalidate: 60 in getStaticProps, but the stale data persisted because the Cache-Control header emitted by middleware was public, s-maxage=3600, overriding the ISR default Incorrect cache-control for stale pages on ISR.
The Working Fix: Cache Key Isolation and Header Correction
We fixed the issue by:
- Removing the Cloudflare Edge TTL on the
/pricingroute so Vercel'ss-maxageandstale-while-revalidateheaders passed through untouched. - Updating the middleware to stop setting
Cache-Control: public, s-maxage=3600on dynamic routes. Instead, we let Next.js emit its default ISR headers:s-maxage={revalidate}, stale-while-revalidate=31536000. - Adding
export const dynamic = 'force-dynamic'to the pricing page to opt it out of the Full Route Cache entirely, since pricing data changes frequently and must never be statically cached How Next.js 15's Full Route Cache Served Stale Prices at Checkout. - Using
revalidateTag('pricing')in the admin webhook instead ofrevalidatePath, so any page consuming the pricing data would be invalidated on the next request.
Real commands used:
curl -sI https://academy.example.com/pricing
# Before fix: x-vercel-cache: HIT, age: 3500, cache-control: public, s-maxage=3600
# After fix: x-vercel-cache: REVALIDATED, cache-control: s-maxage=60, stale-while-revalidate=31536000
Pitfalls We Would Warn an Intern About
- Never set an external Edge TTL on ISR routes. Let Vercel's
s-maxageandstale-while-revalidateheaders pass through untouched. - Do not use
revalidatePathfor routes that are cached at the edge CDN layer. UserevalidateTagand ensure the tag is applied to all relevant fetches. - Always check the
x-vercel-cacheandageheaders withcurl -sIbefore assuming ISR is the problem. Ifageis close to 3600 andx-vercel-cacheisHIT, the edge CDN is holding the page, not ISR. - Do not override
Cache-Controlin middleware for dynamic routes. The default Next.js-on-Vercel ISR headers are correct. - If a route renders user-facing pricing, inventory, or session data, audit it for Full Route Cache eligibility. Add
export const dynamic = 'force-dynamic'before migrating.
What We Would Do Differently Next Time
- We would implement on-demand ISR from the start using
revalidateTaginstead of relying on time-basedrevalidate. This reduces stale data windows to under 500ms with zero polling overhead Postmortem: A Next.js 14 ISR Bug Served Stale Black Friday Deals. - We would define stable, human-readable cache tags (e.g.,
pricing,subscription-plans) instead of using path-based revalidation, so invalidation is decoupled from URL structure. - We would add a staging environment check that runs
curl -sIagainst critical routes after every deploy to verifyx-vercel-cacheandcache-controlheaders are correct. - We would document the cache key strategy for each route in the codebase, so future developers know which routes are static, which are ISR, and which are fully dynamic.
The lesson is simple: ISR is powerful, but it is not magic. It depends on headers, edge layers, and cache keys that are easy to misconfigure. Always verify with curl -sI before assuming the framework is doing what you think it is.
Additional Context: Why Cache Keys Matter Beyond This Incident
Cache keys are the silent contract between your application and the infrastructure layer. When they are wrong, the symptoms are misleading. In our case, the pricing page was technically revalidating on schedule, but the edge CDN was serving a cached copy with a longer TTL. The cache key did not include the user's subscription tier, so even after revalidation, the wrong price was served to the wrong user.
This is a common trap in Next.js applications. Developers assume that because getStaticProps runs at build time, the output is always fresh. But when middleware injects custom headers or when an external CDN sits in front of Vercel, the cache key becomes a composite of multiple layers. Each layer must agree on what constitutes a unique response.
We now enforce a rule: every route that serves user-specific or frequently changing data must have its cache key explicitly defined. For the pricing page, we added a custom cache key that includes the user's subscription status and the current pricing version. This ensures that when a price changes, only the affected users see the update, and the rest of the cache remains valid.
The Hidden Cost of Misconfigured ISR
Beyond the immediate user impact, misconfigured ISR can silently degrade performance. When the edge CDN holds a stale response, it masks the true latency of your application. You might think your page loads in 200ms, but in reality, the user is seeing a cached version that is 30 minutes old. This creates a false sense of reliability.
In our postmortem, we discovered that 12% of our traffic was hitting the stale cache. These users were not reporting issues because the page appeared to work, but they were making decisions based on outdated information. For a pricing page, this is not just a bug; it is a business risk.
We now monitor cache hit rates and age headers in our observability stack. Any route with an age header above 300 seconds triggers an alert. This has caught three similar issues in the past month, all before users noticed.
Conclusion: Treat Cache Keys Like Code
Cache keys are not configuration; they are code. They determine correctness, performance, and user trust. In 2026, with edge computing and distributed caching becoming the norm, the margin for error is shrinking. A single misconfigured header can turn a fast, reliable application into a source of confusion and lost revenue.
We have since open-sourced our cache key validation tool, which runs as part of our CI pipeline. It checks every route for proper cache key isolation and flags any route that relies on time-based revalidation for user-specific data. The tool is available at github.com/example/cache-key-validator.
The next time you deploy a Next.js application, do not just check your code. Check your headers. Check your cache keys. And always, always verify with curl -sI.
Sources you may cite:
- Next.js ISR Revalidation Stuck Serving Stale Pages | AI Tools Guidebook: https://aitoolsguidebook.com/en/articles/nextjs-isr-revalidation-stuck/
- Postmortem: A Next.js 14 ISR Bug Served Stale Black Friday Deals for 2 Hours - Fix with On-Demand ISR - johal.in: https://johal.in/postmortem-nextjs-14-isr-bug-served-stale-black
- How Next.js 15's Full Route Cache Served Stale Prices at Checkout for 3 Hours | Darshan Turakhia: https://darshanturakhia.com/blog/nextjs-15-full-route-cache-stale-checkout
- Your Next.js Page Might Be Caching One User's Data and Serving It to Everyone Else - DEV Community: https://dev.to/anas_sheikh_2/your-nextjs-page-might-be-caching-one-users-data-and-serving-it-to-everyone-else-49lj
- Incorrect cache-control for stale pages on ISR - Issue #49084 - vercel/next.js: https://github.com/vercel/next.js/issues/49084
- Authentication in Next.js 16: Next.js 16 Auth.js | TheCodeForge: https://thecodeforge.io/javascript/nextjs-authentication-authjs-guide/
- I almost shipped a caching system that was doing absolutely nothing. | Murtaza Neher: https://www.linkedin.com/posts/murtaza-neher_nextjs-techlead-ssr-activity-7467213419713044480-7-89
- Postmortem: How a Next.js 15 ISR Revalidation Bug Served Stale Content to 1M Users - johal.in: https://johal.in/postmortem-nextjs-15-isr-revalidation-bug-served-stale
Sources
Related reading
Enjoyed this article?
Back to Blog


