obsidian/wiki/concepts/nextjs-unstable-cache-force-dynamic.md
2026-05-10 22:37:43 +01:00

3 KiB
Raw Blame History

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:

  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

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