docs/01-app/02-guides/server-actions.mdx
A Server Action is a React Server Function invoked through React's action mechanisms, such as <form action>, <button formAction>, or a client-side transition.
You create one by adding the 'use server' directive, then invoke it from a form, or from an event handler or useEffect wrapped in startTransition. For the basics of creating and invoking Server Functions, see Mutating data and the Forms guide.
This page covers the parts of Server Actions that are specific to Next.js: how they commonly map to mutations, how the response carries both returned data and re-rendered UI, how the client dispatches them, the security boundary the framework enforces, and the configuration available.
Next.js dispatches Server Actions one at a time per client. If a user triggers three actions in quick succession, the second waits for the first to finish, then the third waits for the second. This keeps the re-rendered server tree consistent with the action result that produced it.
A consequence: do not rely on Promise.all to parallelize Server Actions from the client. If you need parallel work, do it inside a single Server Action, fetch in parallel from a Server Component, or use a Route Handler for non-mutation requests.
Good to know: This is a property of the client dispatcher, not of Server Functions in general. Server-side, an action runs in its own request and can do anything an async function can do.
When a Server Action triggers an immediate revalidation, Next.js does the work inside one HTTP request: it runs the action, then re-renders the current route server-side. The response that comes back contains both pieces in the same Flight stream:
useActionState or the awaited promise on the client.Your application code does not need a follow-up fetch to see the updated UI for the current page.
A re-render is included in the same response when the action does any of these:
updateTag or revalidatePath to immediately invalidate cached data.refresh to refetch the current route's RSC Payload.cookies(). Setting or deleting a cookie automatically re-renders the current page so the UI reflects the new value.redirect. The response navigates the router and streams the destination's RSC Payload.'use server'
import { revalidatePath } from 'next/cache'
import { auth } from '@/lib/auth'
import { db } from '@/lib/db'
export async function createPost(formData: FormData) {
const session = await auth()
if (!session?.user) throw new Error('Unauthorized')
await db.post.create({
data: {
title: String(formData.get('title')),
authorId: session.user.id,
},
})
revalidatePath('/posts')
}
The mutation, the cache invalidation, and the page re-render all complete in a single roundtrip. Because redirect throws a control-flow exception, any code after it does not run. Place revalidation calls before redirect if the destination needs the fresh data.
revalidateTag with a stale-while-revalidate profile is the exception: it marks the tag for background refresh and does not include a re-render in the action response. The page reflects the change on a later read. An action that does none of the above carries only its return value, and the current route is not re-rendered.
A Server Action runs as a POST request against the page that invokes it. At build time, the 'use server' directive tells the compiler to swap the function's implementation in client bundles for a reference (an action ID plus a dispatcher) that POSTs back to the server. The implementation stays on the server, but the route is reachable to anyone who can send the same POST. Treat every action as an untrusted entry point.
Next.js enforces a few framework-level protections:
Origin is compared to the Host (or X-Forwarded-Host). Mismatches are rejected. Configure serverActions.allowedOrigins for proxy or CDN domains.serverActions.bodySizeLimit when accepting larger payloads.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY to a stable key shared across instances. See Closures and encryption.Framework protections are not a substitute for application-level checks. Inside every action:
FormData, query parameters, and headers as untrusted.For end-to-end patterns including a Data Access Layer, return-value tainting, and rate limiting, see the Data Security guide.
Destructive operations like deletes may warrant stronger handling, such as elevated session checks or re-authentication, and a loud failure when those checks miss.
'use server'
import { auth } from '@/lib/auth'
export async function deletePost(postId: string) {
const session = await auth()
if (!session?.user) throw new Error('Unauthorized')
if (!(await canDelete(session.user, postId))) throw new Error('Forbidden')
await db.post.delete({ where: { id: postId } })
}
If you've enabled the experimental authInterrupts flag, you can throw unauthorized() and forbidden() from next/navigation instead, so Next.js renders the corresponding unauthorized.tsx / forbidden.tsx UI segment automatically.
For example, a client legitimately tells the server which item to act on, but it should not supply the row's contents or ownership. Send a reference (typically an ID) plus the user's change, and re-read the rest from a trusted source using the session. Schema validation (zod or similar) only checks the shape of the input. A well-formed Item object can still refer to a row the caller does not own.
'use server'
import { auth } from '@/lib/auth'
import { db } from '@/lib/db'
// Unsafe: no auth, no ownership check. The whole item, including its id, comes
// from the client, so anyone who can POST here can mark any item complete.
export async function completeItemUnsafe(item: Item) {
await db.item.update({ where: { id: item.id }, data: { completed: true } })
}
// Safe: take only the change, derive identity from the session, look up by ownership.
export async function completeItem(itemId: string) {
const session = await auth()
if (!session?.user) return
const item = await db.item.findFirst({
where: { id: itemId, ownerId: session.user.id },
})
if (!item) return
await db.item.update({ where: { id: item.id }, data: { completed: true } })
}
After mutating data, on-demand revalidation updates the server cache, the client router, or both. Choose based on what needs to change:
updateTag: immediate expiration of a tag. The next read (including the route re-render that ships with the action's response) waits for fresh data. Use when the action needs read-your-own-writes so the user immediately sees their change. Server Actions only.revalidateTag: stale-while-revalidate refresh of a tag with a cache-life profile. Subsequent reads get the stale value while a fresh fetch happens in the background, so the action's own re-render does not wait for the new data.revalidatePath: invalidate by URL path. Use when one route is affected and tagging is overkill.refresh: refetch the current route's RSC Payload without invalidating cached data. Use when the view depends on state outside the cache that the action just changed.When updateTag, revalidatePath, or refresh runs, Next.js re-renders the current route server-side and includes a newly rendered RSC Payload in the action's response, so the page reflects the change in the same roundtrip. revalidateTag with a stale-while-revalidate profile intentionally skips that immediate re-render.
Unlike redirect, none of these throw, so an action can call them and still return a value to the caller. See How revalidation works for the underlying model.
The serverActions option in next.config.js controls framework-level behavior:
/** @type {import('next').NextConfig} */
module.exports = {
experimental: {
serverActions: {
allowedOrigins: ['my-proxy.com', '*.my-proxy.com'],
bodySizeLimit: '2mb',
},
},
}
For the closure encryption key, set NEXT_SERVER_ACTIONS_ENCRYPTION_KEY in the deployment environment. See Self-hosting: Server Functions encryption key for deployment-specific guidance.
Each Server Action is identified by the action ID that is part of its build artifacts. New deployments typically generate new IDs (Next.js rotates them at most every 14 days, even when the source is unchanged), so a client still running the previous build may invoke an action ID that no longer exists. The error surfaces as "Failed to find Server Action".
To minimize disruption:
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY stable across instances so action references remain decryptable everywhere.