Next.js ISR Cache Poisoning on a Client Site: The Revalidation Bug cover image
Back to Blog
TechnologyPublished 28 June 2026· Updated 22 August 2026· 6 min read

Next.js ISR Cache Poisoning on a Client Site: The Revalidation Bug

A single GET /index request from an external scanner poisoned our ISR cache and took down the home page. Here is how we found the root cause and fixed it.

The Incident: Home Page Went Dark on a Self-Hosted Next.js 14 Site

Last Tuesday at 14:23 IST, our monitoring stack lit up. The academy website, a Next.js 14.2.6 app running on Kubernetes in a Mumbai node, started returning 404 for the home page. Not a deploy. Not a config change. Just a 404 that refused to go away.

We use the pages router for this site. The home page lives at pages/index.tsx with export const revalidate = 60. We also have a root-level catch-all at pages/[slug].tsx with dynamicParams: true that calls notFound() for unknown slugs. This setup had been stable for months.

The Client Constraint: Pages Router, Self-Hosted on Kubernetes, No Vercel

The academy website is self-hosted. We run three pods behind an NGINX ingress on Kubernetes. No Vercel. No Edge Runtime. Just Node 18 and a lot of hope. This matters because the cache poisoning advisory from Vercel explicitly calls out that Vercel deployments are not affected Cache Poisoning · Advisory · vercel/next.js · GitHub.

The Trigger: A Single GET /index Request from an External Scanner

At 14:22:47 IST, an external scanner (user agent Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/)) sent GET /index to our ingress. The request was routed to the [slug] dynamic route. Since index is not a known slug, notFound() was called. The response was a 404.

But here is the bug. Next.js used the same internal cache key for /index and /. The notFound() result for /index overwrote the ISR cache entry for /. From that moment on, every request to / returned 404. The cache entry had a stale-while-revalidate lifetime, so it persisted across all three pods.

What We Tried First and What Failed

Attempt 1: Restarting Pods Temporarily Fixed It, But It Came Back

We restarted the pods. The home page came back. Twenty minutes later, the same scanner hit /index again. The 404 returned. Restarting pods was not a fix. It was a band-aid that confirmed the issue was cache poisoning, not a code regression.

Attempt 2: Adding Cache-Control: no-store Headers at the CDN Layer

We added Cache-Control: no-store to the NGINX ingress for all routes. This stopped the CDN from caching the 404, but the ISR cache inside Next.js still held the poisoned entry. The home page stayed dark until we restarted the pods again.

Attempt 3: Blocking /index at the Ingress Level (Too Brittle

We tried blocking /index at the NGINX ingress. This worked, but it was brittle. Any future dynamic route that happened to match a real slug would also be blocked. We needed a real fix.

Root Cause: The Revalidation Bug in Next.js ISR

The issue is documented in GitHub issue #92296 GET /index with [slug] dynamic route poisons ISR cache for /, causing permanent 404 · Issue #92296 · vercel/next.js. The root cause is a cache key collision between /index and /.

The Cache Key Collision Between /index and /

In the pages router, Next.js canonicalizes /index to / internally. Both URLs resolve to the same resolvedPathname. This resolvedPathname is then used as the ISR cache key (ssgCacheKey). When the [slug] route handles /index and calls notFound(), it writes a 404 response to the cache entry for /.

The Role of Root-Level [slug] Dynamic Routes with dynamicParams: true

The [slug] route at pages/[slug].tsx has dynamicParams: true. This means it will match any path that does not match a static route. When /index is requested, it falls through to [slug]. The route calls notFound() because index is not a known slug. The 404 response is cached under the key for /, poisoning the home page.

Why This Only Affects Pages Router, Not App Router

The app router uses a different caching mechanism. It does not canonicalize /index to / in the same way. The advisory confirms that only the pages router is affected Cache Poisoning · Advisory · vercel/next.js · GitHub.

The Working Fix We Kept

Upgrading Next.js to 14.2.10 or Later

The fix is simple. Upgrade to Next.js 14.2.10 or later. The advisory lists 14.2.10 as a patched version Cache Poisoning · Advisory · vercel/next.js · GitHub.

Command: npm install next@14.2.10

npm install next@14.2.10

Verifying the Fix with curl Tests

After upgrading, we ran the reproduction steps from the issue:

curl http://localhost:3000/        # returns 200
curl http://localhost:3000/index   # returns 200 (stale cache)
# Wait 2 seconds
curl http://localhost:3000/        # returns 200 (was 404 before the fix)

The home page stayed up. We also ran the scanner user agent against /index to confirm the 404 no longer poisoned the cache.

Adding a Health Check for /index to Prevent Future Regressions

We added a health check to our Kubernetes deployment that hits /index every 30 seconds. If it returns anything other than 200, the pod is restarted. This is a defensive measure, not a fix, but it gives us early warning.

readinessProbe:
  httpGet:
    path: /index
    port: 3000
  initialDelaySeconds: 10
  periodSeconds: 30

Pitfalls We Would Warn an Intern About

Never Assume /index and / Are Different Routes in ISR

In the pages router, /index and / share the same cache key. This is not a bug in your code. It is a framework behavior that can bite you.

Always Test Dynamic Routes That Call notFound()

If you have a dynamic route that calls notFound(), test it against paths that collide with static routes. /index is the most common offender.

Self-Hosted Deployments Are Not Covered by Vercel Security Patches

Vercel patches are applied automatically on Vercel. Self-hosted deployments require manual upgrades. We missed the 14.2.10 patch because we were on a quarterly upgrade cycle.

CDN Caching Can Amplify the Impact of a Single Poisoned Request

Our NGINX ingress cached the 404 for 60 seconds. This meant the home page was dark for a full minute after each poisoning attempt, even after we restarted the pods.

What We Would Do Differently Next Time

Migrate to App Router to Avoid Pages Router Vulnerabilities

The app router is not affected by this vulnerability. We are planning a migration for Q2 2026.

Implement Request-Level Logging for ISR Cache Writes

We want to log every ISR cache write, including the cache key and the response status. This would have shown us the collision immediately.

Add Automated Tests for Edge Cases Like /index Collisions

We are adding a test that sends GET /index and then checks that GET / still returns 200. This test runs in our CI pipeline.

Set Up Alerting for Sudden 404 Spikes on Critical Routes

We have a Grafana alert for 404s on /, but it was set to a 5-minute window. We are lowering it to 30 seconds.

Sources

Enjoyed this article?

Back to Blog