unstable_cache()

Cache the result of a non-fetch async function, such as a database query, across requests.

Signature
unstable_cache(fetchData, keyParts, options)
  • Added in Next.js v14.0.0
  • Deprecated since PHP v16.0.0

Parameters

ParameterTypeDescription
$fetchData() => Promise<T>The asynchronous function whose result you want to cache. It must return a Promise.
$keyPartsoptionalstring[]Extra key material appended to the cache key. The arguments and a stringified version of the function are already part of the key, so this is only needed for external variables used inside the function that are not passed as parameters — including values captured from a closure.
$optionsoptional{ tags?: string[]; revalidate?: number | false }Controls cache behavior.
$options.tagsoptionalstring[]Tags used to invalidate this entry with revalidateTag or updateTag. Next.js does not use them to identify the function.
$options.revalidateoptionalnumber | false= falseSeconds after which the cache should revalidate. Omit or pass false to cache indefinitely, or until a matching revalidateTag()/revalidatePath() call.

Return value

(...args) => Promise<T>A function that, when invoked, returns a Promise resolving to the cached data; on a miss it runs fetchData, caches the result and returns it.

Errors

Reading uncached request data such as headers() or cookies() inside the cached scope is unsupported; read them outside and pass the values in as arguments.

Changelog

  • PHP v14.0.0unstable_cache introduced.
  • PHP v16.0.0Replaced by the 'use cache' directive. The recommendation is to opt into Cache Components and migrate.

unstable_cache exists because the fetch cache only covers HTTP calls. When your data comes from Prisma, Drizzle, a filesystem read, or an expensive computation, this wraps that function so its result is persisted across requests and deployments and can be invalidated by tag.

Reach for it when

You are on Next.js 15 or on 16 without Cache Components, and you need a non-fetch read to participate in tag-based revalidation. On 16 with cacheComponents enabled, 'use cache' replaces it: the directive goes inside the function instead of wrapping it, and the cache key is derived automatically. Note the one thing the newer directives do not do — entries here survive a new deploy, while 'use cache' keys include the build ID and do not.

What trips people up

  • The key is built from the arguments and the function source, not from closed-over variables. Capture a userId from an outer scope without listing it in keyParts and every user shares one entry.
  • Creating the wrapper inside a component body on every render is legal but pointless work; hoist it to module scope where the arguments are the key.
  • cookies() and headers() are unavailable inside — read them outside and pass values in.

Example

Example · typescript
import { unstable_cache } from 'next/cache'
import { db } from '@/lib/db'

export const getTeamMembers = unstable_cache(
  async (teamId: string) => {
    return db.member.findMany({ where: { teamId } })
  },
  ['team-members'],
  {
    tags: ['members'],
    revalidate: 3600,
  }
)

When to use it

  • Cache a slow aggregate query so a dashboard does not hit the database on every request.
  • Give an ORM read a tag so a Server Action can invalidate it after a write.
  • Cache a filesystem or SDK call that never goes through fetch and so misses the fetch cache.

Frequently asked questions

Is unstable_cache deprecated?

It has been replaced by the 'use cache' directive in Next.js 16, and the recommendation is to opt into Cache Components and migrate. It still works, and it remains the documented option when you need cache entries that persist across deploys, since 'use cache' keys include the build ID.

Why is unstable_cache returning the same data for different users?

Almost always because the distinguishing value is captured from an enclosing scope instead of being an argument. The cache key is built from the function's arguments and its source, so a closed-over userId must be listed in keyParts, or passed as a parameter, to give each user a separate entry.

Can I use cookies() or headers() inside unstable_cache?

No. Accessing uncached data sources such as cookies or headers inside a cache scope is not supported. Read them outside the cached function and pass the values you need in as arguments.

More examples

Keying on a value from params

Lives at app/[userId]/page.tsx. Because userId is read from the closure rather than passed as an argument, it must appear in keyParts — that array is what keeps user A's entry separate from user B's. Note that params is a Promise and is awaited before use.

Example · typescript
import { unstable_cache } from 'next/cache'

export default async function Page({
  params,
}: {
  params: Promise<{ userId: string }>
}) {
  const { userId } = await params

  const getCachedUser = unstable_cache(
    async () => {
      return { id: userId }
    },
    [userId], // add the user ID to the cache key
    {
      tags: ['users'],
      revalidate: 60,
    }
  )

  const user = await getCachedUser()
  return <div>{user.id}</div>
}

Invalidating by tag after a write

Lives at app/actions.ts. The tags passed to unstable_cache are ordinary cache tags, so the same revalidateTag and updateTag calls that invalidate fetch data invalidate ORM queries too.

Example · typescript
'use server'

import { revalidateTag } from 'next/cache'
import { db } from '@/lib/db'

export async function addMember(teamId: string, email: string) {
  await db.member.create({ data: { teamId, email } })

  // Matches options.tags on the cached query
  revalidateTag('members', 'max')
}

Migrating to 'use cache'

The same behavior expressed with the Cache Components API: the wrapper disappears, teamId becomes part of the key automatically as an argument, options.tags becomes cacheTag and options.revalidate becomes a cacheLife profile.

Example · typescript
import { cacheLife, cacheTag } from 'next/cache'
import { db } from '@/lib/db'

// Before: unstable_cache(fn, ['team-members'], { tags, revalidate })
// After, with cacheComponents enabled:
export async function getTeamMembers(teamId: string) {
  'use cache'
  cacheTag('members')
  cacheLife('hours')

  return db.member.findMany({ where: { teamId } })
}

Discussion

  • Be the first to comment on this lesson.