# 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.

```text
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

```text
                 👤 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:

```ts
'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:

```ts
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.

```text
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:

```text
              Load Balancer
                     │
      ┌──────────────┼──────────────┐
      ▼              ▼              ▼
   Server A       Server B       Server C
```

If the update happens on Server A:

```text
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 cache` protects 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.
