Understanding Next.js Caching: How I Optimized App Using ISR, use cache & revalidateTag()

While building My Project, one of the biggest challenges wasn't building the UIβit was deciding how to cache data correctly.
I wanted my application to:
β‘ Feel instant while navigating
π Remain SEO-friendly
π Reduce database queries on Neon
π Show updated data immediately after an admin edits something
π Scale to thousands of users
After reading a lot of documentation and experimenting with different approaches, I finally settled on a hybrid caching strategy using ISR + Data Cache.
The Different Caching Layers
When I first started, I assumed there was only one cache in Next.js.
Turns out there are multiple layers.
Browser
β
βΌ
Route Cache (ISR)
β
βΌ
Data Cache (use cache)
β
βΌ
Neon PostgreSQL
Each layer has its own responsibility.
| Layer | Purpose |
|---|---|
| ISR (Incremental Static Regeneration) | Caches the generated HTML/RSC payload |
use cache |
Caches expensive database queries |
| Router Cache | Makes client-side navigation feel instant |
| Browser Cache | Stores static assets like JS, CSS and images |
My Final Architecture
π€ User
β
βΌ
Next.js Route (ISR)
β
HTML Already Cached?
β β
Yes No
β βΌ
β Server Component
β β
β βΌ
β use cache()
β β
β Cache Hit?
β β β
β Yes No
β β βΌ
β β Neon PostgreSQL
β β β
β βββββββββ
β
βΌ
Return HTML
This means that in most cases:
The page never reaches the database.
If ISR misses, the Data Cache still protects the database.
Even if both caches miss, only then does Neon receive a query.
My Cache Configuration
For expensive database queries I use:
'use cache'
cacheLife('24h')
cacheTag(`restaurant:${id}`)
cacheTag('restaurants')
I intentionally chose a 24-hour TTL.
Why?
Because TTL is only a fallback.
The real freshness comes from tag invalidation.
How Updates Work
Whenever an admin edits a restaurant:
await prisma.restaurant.update(...)
revalidateTag(`restaurant:${id}`)
revalidateTag('restaurants')
The cache is immediately invalidated.
The next request automatically fetches fresh data and recreates the cache.
Admin Updates Restaurant
β
βΌ
Database Updated
β
βΌ
revalidateTag()
β
βΌ
Cache Removed
β
βΌ
Next Visitor
β
βΌ
Fresh Database Query
β
βΌ
New Cache Created
Because of this workflow, I don't need a short cache TTL like 5 or 10 minutes.
Why I Still Use ISR
Initially, I thought I should remove ISR completely.
After experimenting, I decided to keep it.
ISR helps by caching the rendered HTML, meaning many visitors can receive a ready-to-serve page without triggering a server render.
Even if ISR expires, the page regeneration usually doesn't hit the database because the Data Cache is still valid.
So the two caches work together rather than replacing each other.
What About Multiple Servers?
One interesting thing I learned is that revalidateTag() only invalidates the cache on the server where it's executed.
Imagine three application servers:
Load Balancer
β
ββββββββββββββββΌβββββββββββββββ
βΌ βΌ βΌ
Server A Server B Server C
If the update happens on Server A:
Server A β
Fresh
Server B β Old Cache
Server C β Old Cache
To synchronize cache invalidation across every server, a shared system like Redis Pub/Sub or another distributed cache becomes useful.
That's something I plan to add as the application scales.
Why I Didn't Use SWR Everywhere
SWR is fantastic for:
Notifications
Dashboards
User profiles
Live data
Reviews
However, restaurant information changes relatively infrequently.
Using Server Components + ISR + Data Cache keeps the architecture simple while maintaining excellent SEO and minimizing unnecessary client-side requests.
My Current Stack
β‘ Next.js App Router
βοΈ React Server Components (RSC)
ποΈ ISR (Incremental Static Regeneration)
πΎ
use cacheπ·οΈ
cacheTag()π
revalidateTag()π Neon PostgreSQL
βοΈ Netlify
π¦ Cloudflare R2 + CDN
π Redis (for multi-server cache synchronization)
Final Thoughts
One thing this journey taught me is that not every cache solves the same problem.
ISR makes page delivery faster.
use cacheprotects the database.revalidateTag()keeps data fresh without waiting for TTLs.Redis becomes valuable once multiple servers need to share cache invalidation.
Instead of trying to cache everything, understanding which layer should cache what made the biggest difference in both performance and simplicity.





