How I Burned 164K Vercel ISR Writes in 2 Hours
How I Burned 164K Vercel ISR Writes in 2 Hours
If your Vercel ISR write count is exploding, it's probably not a traffic problem.
It's a broken caching chain — and fixing it will likely reveal a second problem underneath.
Here's exactly what happened, how I debugged it, and what I'm still fixing.
The Incident
One of my blog posts got referenced in an external article on thatprivacyguy.com.
Real traffic. Real readers. Sudden spike.
Within 2 hours, the Vercel dashboard looked like this:
- 164K / 200K ISR writes consumed
- 1 month of quota — gone
- 7.5K+ uncached requests hitting origin
_tree.segmentroutes regenerating constantly
It wasn't.
The Debugging Path
My first instinct was ISR configuration.
Wrong direction.
I opened the Cloudflare dashboard and checked the cache hit breakdown.
Nearly every HTML request was returning cf-cache-status: DYNAMIC — meaning Cloudflare was forwarding 100% of requests straight to Vercel origin without caching anything.
Then I checked the actual response headers with curl -I:
cdn-cache-control: (missing)
cf-cache-status: DYNAMIC
Nothing.
Vercel wasn't sending any cache headers that Cloudflare could act on.
So Cloudflare did the safe thing — cached nothing. Every request hit origin. Every origin hit triggered ISR evaluation. Writes compounded fast.
Here's what that looked like in practice:
BEFORE
User Request
↓
Cloudflare (DYNAMIC — no cache)
↓
Vercel Origin
↓
ISR Revalidation
↓
Segment Regeneration
↓
ISR Write
(repeated thousands of times)
After both fixes:
AFTER
User Request
↓
Cloudflare (HIT)
↓
Served From Edge Cache
(no origin hit)
(no ISR regeneration)
(no write storm)
Why ISR Writes Exploded: The Real Equation
Most developers think:
writes = traffic
That's wrong.
The actual equation is:
writes = pages × segments × revalidation frequency
In the Next.js App Router, you're not regenerating "a page."
For every route, the App Router manages a full segment tree:
/blog/post-1
├── layout.tsx → ISR cache entry
├── loading.tsx → ISR cache entry
├── page.tsx → ISR cache entry
├── metadata → ISR cache entry
└── _tree.segment → ISR cache entry
= 4–6 ISR writes per logical page
That's why you see routes like:
/blog_segments/_tree.segment
/index_segments/_tree.segment
One page visit can trigger multiple segment regenerations — each one a separate write.
Now combine that with revalidate = 60 and zero CDN absorption.
Every uncached request hits origin. Every origin hit re-evaluates stale segments. Each evaluation triggers writes across multiple entries.
A modest traffic spike turns into a write storm in minutes.
The Cache Chain: Where It Broke
Here's the full cache stack for a Next.js + Vercel + Cloudflare setup:
Browser
↓
Cloudflare Edge Cache ← ✗ was not caching HTML (DYNAMIC)
↓
Vercel CDN ← ✗ headers missing, no instruction to cache
↓
ISR Cache ← ✗ revalidating every 60s
↓
Origin Compute ← ✓ working, but drowning in requests
Every layer that should have absorbed traffic was passing it through.
Fix 1: Add Cache Headers in Next.js
The first problem: Vercel wasn't sending headers that Cloudflare could act on.
Fixed with a headers() block in next.config.ts:
async headers() {
return [
{
source: "/((?!api/|_next/|favicon.ico|sitemap.xml).*)",
headers: [
{
key: "Cache-Control",
value: "public, s-maxage=2592000, stale-while-revalidate=2592000",
},
{
key: "Cloudflare-CDN-Cache-Control",
value: "s-maxage=2592000, stale-while-revalidate=2592000",
},
{
key: "CDN-Cache-Control",
value: "s-maxage=2592000, stale-while-revalidate=2592000",
},
],
},
];
},
How to read these headers:
s-maxage=2592000— Vercel's edge holds the cache fresh for 1 month
stale-while-revalidate=2592000— CDN can serve stale for up to 1 month while fetching fresh data in the background
- All three headers are aligned to the same 1-month TTL for consistency across cache layers
Cache-Control is the primary header Cloudflare reads. The Cloudflare-CDN-Cache-Control and CDN-Cache-Control headers are CDN-specific overrides — support varies by provider, but they're worth including defensively.
After deploying, curl -I confirmed the headers were present.
But Cloudflare was still returning cf-cache-status: DYNAMIC.
Fix 2: Cloudflare Wasn't Caching HTML in My Setup
This is the part most developers don't know.
Cloudflare's default behavior is to cache only responses with known static file extensions — .css, .js, .png, .woff2, and so on.
Your homepage at / has no file extension.
So by default, regardless of what cache headers you send, Cloudflare treats HTML as uncacheable and proxies straight to origin.
The fix is a Cloudflare Cache Rule:
- Go to Caching → Cache Rules in your Cloudflare dashboard
- Click Create rule
- Name it
Cache HTML Pages
- Condition:
Hostname contains yourdomain.com
- Cache eligibility: Eligible for cache
- Edge TTL: Respect origin header
- Deploy
curl -I https://www.vishwamdhavale.com returned:
cf-cache-status: HIT
HTML was finally being cached at the Cloudflare edge.
Results
| Metric | Before | After |
|---|---|---|
cf-cache-status | DYNAMIC | HIT |
| Cache hit ratio | ~28% | 90%+ |
| ISR writes/hour | Exploding | Stable |
revalidate | 60s | 2592000s (1 month) |
| Origin pressure | High | Minimal |
The incident was caused by a chain of three compounding problems:
- Missing cache headers from Vercel
- Cloudflare defaulting to
DYNAMICfor HTML
- ISR
revalidate = 60with no CDN layer absorbing traffic
The New Problem: Two Caches That Don't Talk
Fixing Cloudflare caching revealed a second architectural issue.
The setup now has two independent cache layers:
Vercel ISR Cache → s-maxage=2592000 (1 month)
Cloudflare Edge Cache → s-maxage=2592000, stale-while-revalidate=2592000 (1 month)
These don't communicate.
When Vercel revalidates a page — either on-demand or time-based — Cloudflare has no idea. It continues serving the old cached version until its own TTL expires. That could be hours, or in a worst case, much longer given the stale-while-revalidate window.
So publishing a new blog post right now means:
- Vercel knows about it immediately
- Cloudflare keeps serving the old page for up to 1 month
- Readers hitting Cloudflare edge nodes see stale content
What's Next: Purging Both Caches on Publish
The correct fix is on-demand revalidation that clears both layers simultaneously.
When a post is published in Payload CMS, the webhook should:
- Call
revalidatePath()orrevalidateTag()to clear Vercel's ISR cache
- Call the Cloudflare Purge API to clear the edge cache for that URL
// revalidateNextjs.ts (Payload CMS hook)
await fetch('https://www.vishwamdhavale.com/api/revalidate', {
method: 'POST',
body: JSON.stringify({ path: /blog/${slug} }),
});
await fetch(https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache, {
method: 'POST',
headers: {
'Authorization': Bearer ${CF_API_TOKEN},
'Content-Type': 'application/json',
},
body: JSON.stringify({ files: [https://www.vishwamdhavale.com/blog/${slug}] }),
});
That gives you:
- Long TTL for CDN stability under traffic
- Instant cache invalidation on publish
- No write storms
- No stale content after publishing
Key Takeaways
- Cloudflare does not cache HTML by default — you need a Cache Rule
- Vercel won't send CDN-friendly headers unless you explicitly configure them
- ISR writes scale by
pages × segments × revalidation frequency, not raw traffic
- App Router regenerates entire segment trees — one page = multiple write entries
- Adding CDN caching without a purge strategy creates a double cache sync problem
- On-demand revalidation should clear both Vercel and Cloudflare simultaneously
Written by Vishwam Dhavale
Full stack developer building scalable web & mobile systems. Founding Engineer with a passion for clean architecture and great DX.
Related Articles
Chrome Installed a 4GB LLM on My Machine. Here's What I Found Out.
I was clearing disk space and found Gemini Nano silently living in my Chrome config. What it is, why it's there, and what compression research tells us about where this is heading.
How I Built My Developer Portfolio — From Idea to Production
A deep dive into building a modern developer portfolio with Next.js 16, Framer Motion, Three.js, and an interactive terminal. The story behind the design decisions, tech stack choices, and the journey from concept to production.
What Actually Happens When Cloudflare Goes Down (DNS, CDN, Edge Explained)
A deep dive into DNS, CDN, and edge computing failures when Cloudflare experiences an outage — and why so much of the internet depends on it.