Debugging Server Actions that appear to do nothing starts with one question: did the mutation run and the page fail to refresh, or did the action never run at all?
Those two halves have completely different fixes, so decide which one you are in before changing any code. The table below maps the symptom you can see to the cause behind it.
| Symptom | Likely cause | Fix |
|---|---|---|
| Data changed but the page is stale | No revalidation | Call revalidatePath or updateTag |
| Nothing happens and no error | The throw was swallowed | Remove the try/catch |
| Button never shows pending | Call outside a transition | Wrap it in startTransition |
| Works locally, breaks after deploy | Stale action ID | Redeploy together, refresh |
Forgot to revalidate
The most common cause is a mutation that succeeds while the page keeps showing old data. The action wrote the value, but nothing told Next.js to refresh the route.
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
let title = 'Draft'
export async function renamePost(formData: FormData) {
title = formData.get('title')?.toString() ?? title
revalidatePath('/posts')
}The assignment changes the stored title. Without the revalidatePath call, nothing tells Next.js that the route's cached output is now wrong, so the page keeps rendering the old copy.
The tell is a POST that completes with a 200 in the network tab while the text on screen never changes. If you see that, check revalidation before anything else. See revalidating data after a Server Action for the full set of calls.
The error was swallowed
If you wrap the action body in a try/catch and forget to re-throw, a thrown error disappears and the visitor sees nothing. The same trap swallows a redirect, which stops the navigation entirely.
A caught error should still surface somewhere, either as returned state the form renders or as a re-thrown error. A catch with no feedback turns a loud failure into a silent one.
The redirect case catches people out most often, because redirect works by throwing. A try block wrapped around it swallows the navigation and the visitor simply stays put. Call redirect after the catch block, never inside the try.
The action never runs on the server
Without the use server directive at the top of the file, the function is ordinary module code. Import it into a Client Component and it gets bundled into the browser, where a database call has nothing to connect to.
// app/actions.ts
'use server'
export async function createPost(formData: FormData) {
const title = formData.get('title')?.toString()
return { title }
}The directive is what swaps the implementation for a server reference at build time. Missing it is easy to do when moving a function out of a component and into its own file, since the code itself does not change.
A server-only import that throws in the browser is the tell. Adding import 'server-only' to modules that must never ship to the client turns that runtime surprise into a build error instead. See how to mutate data with a Server Action for the working pattern.
Stale build after deploy
After a redeploy, a browser still running the previous build can call an action ID that no longer exists. The request fails with this exact message:
Failed to find Server ActionNext.js generates non-deterministic action IDs that change between builds, so an ID from the old bundle no longer resolves. The request returns without running the action and the message appears in your server logs.
The fix is to serve client assets and Server Actions from the same deployment. When self-hosting across several instances, set NEXT_SERVER_ACTIONS_ENCRYPTION_KEY to a shared base64 AES key at build time, since the key is embedded in the build output. On Vercel, Skew Protection keeps the previous version's actions reachable after a deploy, which closes the window entirely.
A related message, Server Reference ID did not match the expected format, means the client and server are on Next.js versions that generate different ID formats. It also shows up in traffic from automated security scanners sending malformed requests.
Queued behind a stuck action
Next.js dispatches Server Actions one at a time per client. If an earlier action hangs on a slow request, every action after it waits its turn, so a later button looks broken when the real problem is upstream.
The symptom is a click that does nothing until an unrelated request finishes, then everything happens at once. Look for one slow action in the sequence rather than debugging the button you just pressed.
Parallelism has to happen inside a single action, or move to a Route Handler for work that is not a mutation. Promise.all across separate actions does not help, because the queue is on the client dispatcher.
Check the obvious first
Before digging into build keys, confirm the basics. Is the action actually wired to the form or the handler, or is it imported and never called? Is the returned value being read, or quietly discarded?
The network tab settles most of this in seconds. A POST to the current page URL means the action dispatched, so the problem is on the server or in revalidation. No request at all means the call never left the browser, which points back at the transition or the wiring.
For the pending state that never lights up, see calling Server Actions from event handlers and useTransition.
Rune AI
Key Insights
- Check revalidation first when data changes but the page is stale.
- Do not wrap the action body or redirect in a swallowing try/catch.
- Confirm the file starts with use server so the code runs server-side.
- Failed to find Server Action means client and server builds differ.
- Actions queue one at a time, so a hung action blocks the rest.
Frequently Asked Questions
Why does my data change but the page still shows old values?
Why do I get Failed to find Server Action?
Why is my isPending flag always false?
Conclusion
A Server Action that does nothing usually failed to revalidate, swallowed its own error, or never ran on the server. Check revalidation first, then the error path, then the build, and the silent failure becomes visible.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.