---
name: nextjs-unstable-cache-force-dynamic
description: unstable_cache with tag-based revalidation vs force-dynamic — ISR pattern for rarely-changing external API data in Next.js 15
type: concept
---

# Next.js 15 — `unstable_cache` vs `force-dynamic` for External API Routes

## The Problem with `force-dynamic`

```ts
// BAD — disables ALL caching, hits DB + external API on every request
export const dynamic = 'force-dynamic'

export async function GET() {
  const tariffs = await fetchFromExternalAPI()
  // ...
}
```

`export const dynamic = 'force-dynamic'` is a segment-level directive that opts the entire route out of Next.js's Data Cache. Every request triggers:
1. A fresh fetch to the external API
2. A fresh DB query
3. Full server rendering overhead

This is appropriate for user-specific data that changes per-request (e.g., cart contents, auth status). It is **wrong** for data that is the same for all users and changes infrequently (e.g., pricing tariffs, product catalogs).

## The Fix — `unstable_cache` with Tag-Based Revalidation

```ts
import { unstable_cache } from 'next/cache'

// Remove: export const dynamic = 'force-dynamic'

const getCachedTariffs = unstable_cache(
  async () => {
    const [ezyTariffs, dbTariffs] = await Promise.all([
      fetchFromExternalAPI(),
      payload.find({ collection: 'tariffs', limit: 1000 }),
    ])
    return mergeTariffs(ezyTariffs, dbTariffs.docs)
  },
  ['tariffs'],              // cache key
  {
    revalidate: 300,        // revalidate every 5 minutes (seconds)
    tags: ['tariffs'],      // tag for on-demand invalidation
  }
)

export async function GET() {
  const tariffs = await getCachedTariffs()
  return Response.json(tariffs)
}
```

### On-Demand Revalidation (e.g., after admin update)

```ts
import { revalidateTag } from 'next/cache'

// Call from a webhook or admin action after tariffs change
revalidateTag('tariffs')
```

## Choosing the Right Strategy

| Data type | Strategy | Why |
|-----------|----------|-----|
| Per-user, changes every request | `force-dynamic` or `no-store` | No shared cache possible |
| Shared, changes rarely (tariffs, config) | `unstable_cache` with `revalidate` | Serve stale, refresh in background |
| Static, never changes at runtime | Default (static generation) | Build-time bake-in |
| Shared, changes after specific events | `unstable_cache` with `tags` | Invalidate precisely |

## Naming Note

`unstable_cache` is stable in practice despite the prefix — Vercel ships production apps on it. The `unstable_` prefix signals it may be renamed in a future API stabilization. The `cache()` function (React 19's `use cache` directive) is the eventual successor.

## Performance Impact

For a tariff endpoint called N times/minute:
- `force-dynamic`: N × (external API latency + DB query)
- `unstable_cache` (5 min TTL): 1 × (external API latency + DB query) per 5 min + N × cache read

At 100 req/min this is a ~99.7% reduction in external API calls.

**Source:** daily/2026-05-09.md | 2026-05-09
