ISR Stale Data Fix: Cache Keys, Headers, and Edge TTL cover image
Back to Blog
TechnologyPublished 30 June 2026· Updated 22 August 2026· 6 min read

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.

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:

  1. Removing the Cloudflare Edge TTL on the /pricing route so Vercel's s-maxage and stale-while-revalidate headers passed through untouched.
  2. Updating the middleware to stop setting Cache-Control: public, s-maxage=3600 on dynamic routes. Instead, we let Next.js emit its default ISR headers: s-maxage={revalidate}, stale-while-revalidate=31536000.
  3. 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.
  4. Using revalidateTag('pricing') in the admin webhook instead of revalidatePath, 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-maxage and stale-while-revalidate headers pass through untouched.
  • Do not use revalidatePath for routes that are cached at the edge CDN layer. Use revalidateTag and ensure the tag is applied to all relevant fetches.
  • Always check the x-vercel-cache and age headers with curl -sI before assuming ISR is the problem. If age is close to 3600 and x-vercel-cache is HIT, the edge CDN is holding the page, not ISR.
  • Do not override Cache-Control in 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 revalidateTag instead of relying on time-based revalidate. 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 -sI against critical routes after every deploy to verify x-vercel-cache and cache-control headers 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:

Enjoyed this article?

Back to Blog