Fix “Failed to find Server Action” after deploying Next.js.
The browser called a Server Action by an ID the running server doesn’t know. Next.js generates new action IDs on each build, so a page loaded before your deploy, a second server on another build, a different encryption key or cached HTML all cause it. Set deploymentId, keep one build per release, and reload stale tabs.
Facts checked
What the error means
When a client component calls a Server Action, the browser sends a POST with a Next-Action header holding the action’s ID. The server looks that ID up in the build it is running. If the build has no action with that ID, it logs Error: Failed to find Server Action "7f3a…". This request might be from an older or newer deployment. and answers with a 404. In the browser console the same failure reads Server Action "7f3a…" was not found on the server.
Next.js makes these IDs non-deterministic on purpose, for security, and they can change between builds. So the error almost always means the page in the browser and the server answering it came from different builds.
A second message, Server Reference ID did not match the expected format, is different: the ID isn’t even shaped like one this Next.js version produces. The Next.js docs list two causes, client and server on Next.js versions with different ID formats, or malformed requests such as automated security scanners. If it shows up in bursts from one IP, it is probably a scanner and you can ignore it.
The causes, and the fix for each
Work down this table. The first row is the one most people hit, because it happens on every deploy to any host.
| Cause | How to recognise it | Fix | On Canner |
|---|---|---|---|
| A tab opened before the deploy † † | Errors cluster in the minutes after each deploy, then stop | Set deploymentId so a stale tab reloads instead of posting old IDs; catch the failed call in the client | Happens: one process serves production, so old tabs meet the new build. CANNER_DEPLOYMENT_ID is set during every build |
| Two servers on different builds † † | Errors at random, any time, roughly in proportion to traffic on each server | Run the same build output everywhere; build once, ship that artifact | Doesn’t apply: one process per project, and a deploy replaces it |
| Different encryption keys † | Several instances built separately; actions with bound arguments or closures fail | Set the same NEXT_SERVER_ACTIONS_ENCRYPTION_KEY at build time for every instance | Not needed for a single process; set it if you also run copies elsewhere |
| Cached HTML from an earlier build † | Errors persist long after a deploy, on cacheable pages only | Purge the CDN on deploy, or don’t cache HTML that contains forms | Canner’s response cache is purged on every successful production deploy; an outside CDN is yours to purge |
| Malformed or scanner requests † | did not match the expected format, often from one IP hitting many URLs | Nothing to fix in your app; rate-limit or block the source if it’s noisy | Shows in your app’s logs like any other request |
† Competitor details come from each vendor’s own published pages on the dates shown; prices and plans change, so check the vendor before you decide. A tab opened before the deploy (2026-10-29) · A tab opened before the deploy (2026-10-29) · Two servers on different builds (2026-10-29) · Two servers on different builds (2026-10-29) · Different encryption keys (2026-10-29) · Cached HTML from an earlier build (2026-10-29) · Malformed or scanner requests (2026-10-29)
Stale tabs: set a deployment ID
Someone opens your form at 10:00. You deploy at 10:05. At 10:07 they click Save. Their tab still runs the 10:00 JavaScript, which posts the 10:00 action ID to a server running the 10:05 build. That is the whole bug, and no host can make it go away completely unless it keeps the old build running alongside the new one.
Next.js has a setting for it. With `deploymentId` set, client navigations send an x-deployment-id header, the server replies with its own, and when they differ the client does a full page reload instead of a client-side navigation. The user lands on the new build and its new action IDs. Any value that changes each deploy works:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// Any value that changes on every deploy. Canner sets CANNER_DEPLOYMENT_ID
// during each build; elsewhere use your git SHA or CI build number.
deploymentId: process.env.CANNER_DEPLOYMENT_ID,
};
export default nextConfig;Catch the failure in the client
A deployment ID helps on the next navigation. A user who sits on one page and clicks a button can still send one stale call. When an action can’t be found, the promise the client awaits is rejected with an error that unstable_isUnrecognizedActionError from next/navigation recognises (in Next.js 16.2; the unstable_ prefix means it may change). Handle it instead of leaving a dead button:
"use client";
import { unstable_isUnrecognizedActionError } from "next/navigation";
import { saveDraft } from "./actions";
export function SaveButton({ id }: { id: string }) {
async function onClick() {
try {
await saveDraft(id);
} catch (err) {
// This tab was loaded before the last deploy.
if (unstable_isUnrecognizedActionError(err)) {
if (confirm("This page was updated. Reload to continue?")) location.reload();
return;
}
throw err;
}
}
return <button onClick={onClick}>Save</button>;
}NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
Next.js encrypts the variables a Server Action closes over before sending them to the browser. By default each build makes its own key. When more than one instance serves the same site, an action encrypted by one instance can’t be decrypted by another, and Next.js reports it as “Failed to find Server Action”.
The fix from the self-hosting guide is one shared key: base64, 16, 24 or 32 bytes. It is read at build time and embedded in the build output, so setting it only on the running server does nothing.
- On Canner, add it in the project’s Env Vars tab with the production or all scope and mark it sensitive. A variable scoped runtime only never reaches the build, so the key would not be embedded. See scopes.
- A shared key does not make action IDs stable between two different builds. It fixes multi-instance decryption, not stale tabs.
# Generate once, keep it in your secrets store
openssl rand -base64 32
# It must be present when `next build` runs
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=<the 44-character value> next buildWhat Canner does at cutover
Canner runs each project as one long-lived next start process (how a deploy runs). A production deploy builds the new version, runs your release command if you set one, then starts the new build in place of the old one. It takes traffic once it answers the health check. Two builds never answer production at the same time, so the mixed-instance causes above can’t happen.
The stale-tab cause still can. Canner doesn’t keep the previous build running for old tabs the way Vercel’s Skew Protection does, so set deploymentId and handle the rejected call as shown. Each pull-request preview is a separate build at its own address, so it doesn’t mix with production.
Two more things change the build under your users: an auto-rollback rebuilds your last good commit, and a manual rollback rebuilds an earlier one. Both are new builds with new action IDs, so treat them like any other deploy.
Questions
Why does “Failed to find Server Action” only happen in production?
In development there is one server and it rebuilds as you edit, so the page and the server always match. In production a browser can keep a page from the previous build open for hours. The next deploy changes the action IDs underneath it.
Do I need NEXT_SERVER_ACTIONS_ENCRYPTION_KEY?
Only when more than one server answers for the same site and they must decrypt each other’s actions. Set it before next build, because the key is embedded in the build output. It does not fix tabs left open across a deploy; deploymentId does.
Can I make Server Action IDs the same across builds?
Not reliably. Next.js documents the IDs as non-deterministic and says they can change between builds. Plan for old tabs instead: a deployment ID to force a reload, and a catch around the call.
Does Canner keep the old version running during a deploy?
No. One process serves production, and a deploy replaces it once the new build passes its health check. See builds and rollbacks.
Related
- GuideDeploy Next.js without Vercel: what changes
- GuideNext.js env variables undefined in production
- GuideVite import.meta.env undefined in production
- GuideBuild failed: read the log and fix the cause
- GuideJavaScript heap out of memory during build
- GuideWhere to run database migrations in a deploy
Deploy your Next.js app in Montréal.
One next start process, a database next to it, and your data in Quebec. Deploy from GitHub or the CLI.