updateTag()

Expire a cache tag from a Server Action so the caller immediately sees their own write.

Signature
updateTag(tag: string): void

Parameters

ParameterTypeDescription
$tagstringThe cache tag to expire. Case-sensitive, max 256 characters. The tag must already be attached to cached data via fetch's next.tags option or a cacheTag() call inside a 'use cache' scope.

Return value

voidNothing. The tagged data is expired immediately and the current page is refreshed with fresh data.

Errors

Throws when called anywhere other than a Server Action — Route Handlers, Client Components and every other context error with "updateTag can only be called from within a Server Action".

updateTag is the read-your-own-writes primitive. A user submits a form, the action writes to the database, and the very next render must show the new row — not a cached list from three seconds ago. It expires the tagged data and refreshes the current page in one step, so no stale copy is served in between.

When to reach for it

Anywhere a human just caused the change and is waiting to see it: creating a post, toggling a setting, deleting a comment. Tag the data with cacheTag or fetch's next.tags, then expire those tags at the end of the action. It composes with redirect — the destination renders with fresh data rather than the cached version the user would otherwise land on.

What trips people up

  • It is Server-Action-only. A webhook, cron endpoint, or any Route Handler must use revalidateTag instead; calling it there throws.
  • It makes the next read wait for fresh data on purpose. That is correct for a form submit and wasteful for a background sync of content nobody is staring at.
  • Expiring a tag no cached entry carries silently does nothing — a typo between cacheTag('posts') and updateTag('post') looks exactly like a caching bug.

Example

Example · typescript
'use server'

import { updateTag } from 'next/cache'
import { redirect } from 'next/navigation'

export async function createPost(formData: FormData) {
  const title = formData.get('title')
  const content = formData.get('content')

  const post = await db.post.create({
    data: { title, content },
  })

  // Any page listing posts
  updateTag('posts')
  // The detail page for this one
  updateTag(`post-${post.id}`)

  redirect(`/posts/${post.id}`)
}

When to use it

  • Show a newly created post immediately after the form action redirects to it.
  • Refresh a settings page the moment a user saves, with no stale flash.
  • Invalidate both a list tag and an item tag in one mutation, so index and detail pages agree.

Frequently asked questions

Why does updateTag throw "can only be called from within a Server Action"?

Because it is restricted to Server Actions by design — it expires the tag and refreshes the current page, and only an action has a page in flight to refresh. In a Route Handler, webhook, or Client Component, use revalidateTag instead.

Should I use updateTag or revalidateTag after a form submit?

updateTag. It guarantees the next read waits for fresh data, so the user sees the change they just made rather than a stale cached list. revalidateTag with a profile serves stale content while revalidating in the background, which is right for webhooks but wrong for read-your-own-writes.

Does updateTag work with fetch caching or only 'use cache'?

Both. It expires any entry carrying the tag, whether the tag came from fetch(url, { next: { tags: ['posts'] } }) or from a cacheTag('posts') call inside a 'use cache' scope.

More examples

The tags this action expires

Lives at lib/posts.ts. This is the half people forget: updateTag only has an effect on entries that were tagged when they were written. The 'max' lifetime is safe here precisely because the mutation expires the tag on demand.

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

export async function getPosts() {
  'use cache'
  cacheTag('posts')
  cacheLife('max')

  return db.post.findMany({ orderBy: { createdAt: 'desc' } })
}

export async function getPost(id: string) {
  'use cache'
  cacheTag(`post-${id}`)
  cacheLife('max')

  return db.post.findUnique({ where: { id } })
}

Wrong context, and the fix

Lives at app/api/posts/route.ts. The first call throws at request time. The point of the restriction is that only a Server Action has a page to refresh — a Route Handler has no current render to hand fresh data back to.

Example · typescript
import { revalidateTag, updateTag } from 'next/cache'

export async function POST() {
  // Error: updateTag can only be called from within a Server Action
  updateTag('posts')

  // Route Handlers use revalidateTag instead
  revalidateTag('posts', 'max')

  return Response.json({ ok: true })
}

Pairing with a path purge

Shows the belt-and-braces pattern. updateTag covers data shared across routes; revalidatePath covers the rendered route, including anything on it that was never tagged.

Example · typescript
'use server'

import { revalidatePath, updateTag } from 'next/cache'

export async function updatePost(id: string, formData: FormData) {
  await db.post.update({
    where: { id },
    data: { title: formData.get('title') as string },
  })

  updateTag('posts') // every page reading the tag
  revalidatePath('/blog') // the route itself, tags aside
}

Discussion

  • Be the first to comment on this lesson.