As of October 2026 — Next.js 16 App Router, deployed on Vercel, backed by a serverless database that wakes up only when queried. Cache-layer lifetimes can change between versions, so re-measure them in your own setup with the steps under "How do you measure a cache layer's lifetime?".
Say your site has tens of thousands of pages and builds none of them at build time — each page is generated on its first request. Store locators by region, product detail pages, directories of schools or clinics: lots of pages, few changes per day. In Next.js ISR (Incremental Static Regeneration) terms, that means generateStaticParams returns an empty array and data is held with unstable_cache or tag-based revalidation. Your database is serverless and scales to zero, so the bill should be small. Yet on every day you deploy, the database stays awake all day, and the cost numbers you were trying to measure jump around. The code is fine. The deploy is the problem.
What is a deploy on a site that renders on request?
On a site that renders pages on request, a deploy is not just shipping code — it is an event that empties the cache. Pages built at build time ship with the deploy, but pages generated on request, and the data they hold, usually vanish the moment the deployment changes. Every following request rebuilds from scratch. Human visitors open a handful of pages; search crawlers follow your sitemap and open tens of thousands, one by one — and each one wakes the database.
So a site like this needs to do two things together:
- Measure which cache layers survive a deploy. Measure it; don't trust docs or code comments
- Treat deploy count and order as a cost. One deploy = one cache wipe = one warm-up

Why do cache layers behave differently across a deploy?
A single page sits behind several caches: one that holds the whole rendered page, one inside it that holds query results, and a key-value store the platform offers separately. Each layer ties its lifetime to something different — some start fresh with every deployment, some outlive it. Both surprise you:
- A layer that dies with the deploy wakes the database on the first request after every deploy. Cost leaks
- A layer that outlives the deploy makes a new build read old data. Stale pages ship
You usually can't tell which from the name. The framework docs describe a persistent "data cache", but whether the hosting platform scopes it per deployment is often not written down (Next.js unstable_cache). The platform's runtime cache, by contrast, uses keys you choose and is documented as readable across deployments (getCache in the Vercel Functions package). Put the layer whose lifetime is explicitly documented on the inside, and even when the outer layer is wiped by a deploy, requests stop before reaching the database.
How do you stack a layer that survives deploys?
Wrap data reads in two layers: a deploy-surviving key-value cache inside, the framework's page/data cache outside.
- Inside the query function, first read the runtime cache by key. Name the key after what the data is (
store:seoul:gangnam) — never include a deployment ID - Only on a miss, query the database and write the result to the runtime cache
- Leave the outer page cache as it is. When a deploy wipes it, pages are rebuilt, but the data comes from the inner layer
- Invalidate both layers with the same tag, together. Clear only one, and the other refills it with old data
With this in place, pages regenerate after a deploy without any warm-up while the database stays asleep, and statically built paths stop waking it at build time too.
What if you can't add a deploy-surviving layer?
If not all data can live in a deploy-surviving layer, reduce and batch the deploys themselves:
- Don't deploy commits that only change docs. If pushing to main deploys, bundle doc changes into the next code deploy
- When a data reload and a deploy fall on the same day, fix the order. Load → deploy → warm up once. Reverse it and you wipe the cache twice
- Put warm-up into the deploy procedure. Request every sitemap URL once to refill the cache. It takes time in proportion to page count (about 17 minutes for roughly 19,000 pages in the case below), so plan for the database being awake that long
- Don't deploy while measuring cost. A single deploy mixes the cost of a cache wipe into the period, and you have to start the measurement over
How do you measure a cache layer's lifetime?
Even if a comment says "this cache survives deploys", don't trust it. The test is simple:
1. Open a view where you can see database connections (or when it woke up)
2. Load one page twice; confirm the second load does not wake the DB (the cache works)
3. Change one line of code and deploy
4. Load the same page again
- DB wakes up → this layer dies with the deploy (cost risk)
- DB stays asleep, old value shown → this layer outlives the deploy (stale-data risk)
5. Write the result for each layer, with the date, in a comment next to the cache setup
Three times this happened to us
- One deploy changed a whole day's database cost. On a site of about 19,000 on-request pages, every deploy emptied the cache and crawlers spent the day rebuilding pages one by one, waking a database that should have been asleep. We added a full sitemap warm-up (about 17 minutes) to the deploy procedure and stopped deploying doc-only commits. Once, a deploy landed in the middle of a cost measurement and we had to restart it.
- A data cache "that survives deploys" did not. Another site's code comment said the data cache outlived deploys, but the database activity log showed it waking on the first request after every deploy. Putting a runtime cache documented to survive deploys on the inside let pages regenerate without warm-up while the database stayed asleep.
- The opposite: a response that should have expired survived into new builds. A build-once route cached the response of its database read, so neither revalidation nor a fresh build picked up new data. We noticed because the sitemap listed fewer posts than actually existed. We tagged that read explicitly and cleared that tag first.
No errors in any of the three. Each cache did exactly what its lifetime said — what was wrong was our guess about that lifetime.
PreviousDon't let the model guess the current state — pass time and progress as flat input on every call