3 KiB
| name | description | type |
|---|---|---|
| nextjs-unstable-cache-force-dynamic | unstable_cache with tag-based revalidation vs force-dynamic — ISR pattern for rarely-changing external API data in Next.js 15 | concept |
Next.js 15 — unstable_cache vs force-dynamic for External API Routes
The Problem with force-dynamic
// 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:
- A fresh fetch to the external API
- A fresh DB query
- 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
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)
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