If you are seeing errors like:
Error: Failed to find Server Action "..."
This request might be from an older or newer deployment.
There are usually two different problems mixed together:
- a real deployment skew problem;
- malformed requests hitting your app with garbage
next-actionheaders.
The official Next.js error page focuses on the first case. The GitHub discussion #87851 shows that many teams were also hit by the second one, especially bot traffic sending values like next-action: x.
The real fix
The official explanation is straightforward: Next.js generates encrypted, non-deterministic Server Action IDs, and those keys are recalculated between builds. In a self-hosted multi-server setup, that can break Server Actions if different instances serve different builds or different encryption keys.
According to the docs, the first thing to do is set NEXT_SERVER_ACTIONS_ENCRYPTION_KEY at build time:
openssl rand -base64 32
Use the generated value during next build:
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY="your-base64-key" next build
This part matters a lot: the Next.js docs explicitly say that the key is embedded into the build output and then used automatically at runtime. So setting it only for next start is too late.
If you self-host Next.js, also follow the self-hosting guide:
- deploy the same build artifact to all containers and servers;
- if you rebuild per environment or per node, configure a stable
generateBuildId; - set a
deploymentIdto help Next.js detect version skew during rolling deploys.
Example next.config.ts:
import type { NextConfig } from "next";
const deploymentId = process.env.DEPLOYMENT_VERSION ?? process.env.GIT_HASH;
if (!deploymentId) {
throw new Error(
"DEPLOYMENT_VERSION or GIT_HASH must be set during next build.",
);
}
const nextConfig: NextConfig = {
deploymentId,
generateBuildId: async () => deploymentId,
};
export default nextConfig;
And make sure your CI builds once and promotes the same image or artifact everywhere instead of rebuilding separately for each server.
If you are behind a reverse proxy
If Server Actions are called through a proxy domain, load balancer, or CDN, review experimental.serverActions.allowedOrigins. Next.js compares the action request origin with the host to protect against CSRF, and the docs say that only the same origin is allowed by default.
Example:
module.exports = {
experimental: {
serverActions: {
allowedOrigins: ["app.example.com", "*.app.example.com"],
},
},
};
This is not the main fix for the error above, but it is easy to miss in setups where traffic flows through another hostname.
Why next-action: x is different
The GitHub discussion is useful because it shows another pattern: some teams kept seeing the exact same error even after setting NEXT_SERVER_ACTIONS_ENCRYPTION_KEY.
One especially telling detail was malformed next-action header values such as:
next-action: x
next-action: test
That does not look like a real Server Action ID from a deployed Next.js app. It looks more like bots probing for weaknesses, and it matches reports in the discussion thread.
So if the value is obviously malformed, you should block it early and avoid wasting application capacity on requests that can never succeed.
Block malformed Server Actions with proxy.ts
If you want an in-app defensive layer, proxy.ts is a good fit. The Proxy docs note three things that are useful here:
- Proxy runs before filesystem routes;
- you can return a response directly from Proxy;
- matchers can use
hasandmissingheader checks.
Here is a simple proxy.ts that only looks at requests carrying a next-action header and rejects obviously bad values:
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
const MIN_ACTION_ID_LENGTH = 10;
export function proxy(request: NextRequest) {
if (request.method !== "POST") {
return NextResponse.next();
}
const actionId = request.headers.get("next-action");
if (!actionId) {
return NextResponse.next();
}
if (actionId.length < MIN_ACTION_ID_LENGTH) {
return new NextResponse("Bad Request", { status: 400 });
}
return NextResponse.next();
}
export const config = {
matcher: [
{
source: "/:path*",
has: [{ type: "header", key: "next-action" }],
},
],
};
A few notes:
- keep the check conservative;
- do not hardcode a hex-only regex unless you fully accept future format changes;
- keep the matcher static; the Proxy docs say matcher values must be constants for build-time analysis;
- the community workaround in the GitHub discussion used a minimum length check because real action IDs were much longer, while garbage probes were tiny.
That last point is important. This is a heuristic filter, not a protocol guarantee.
Better than proxy.ts: block it before Next.js
If you already have Nginx, Caddy, Traefik, Cloudflare, or another edge layer, reject malformed next-action requests there first.
That is the better place for it because:
- the request never reaches Node.js;
- your logs stay cleaner;
- bad traffic does not burn app CPU at all.
The GitHub discussion includes examples for Caddy and Traefik. I would treat proxy.ts as a second line of defense or as the simplest option when you do not control the outer proxy.
Also, do not expose your app containers directly to the public internet if you can avoid it. Put the reverse proxy in front and only allow private network traffic to the Next.js instances.
Fail the build if the setup is incomplete
This error is painful partly because it often shows up only after deployment. The easiest way to improve that is to fail the build when the required deployment metadata is missing.
You cannot fail the build because bots might someday send malformed next-action headers. But you can fail the build if your app is missing the configuration that makes those requests harmless or makes rolling deploys safe.
For example, validate the Server Actions encryption key in next.config.ts:
import type { NextConfig } from "next";
function requireServerActionsKey(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`${name} must be set during next build.`);
}
const bytes = Buffer.from(value, "base64");
const normalized = bytes.toString("base64").replace(/=+$/, "");
const incoming = value.replace(/=+$/, "");
if (normalized !== incoming) {
throw new Error(`${name} must be valid base64.`);
}
if (![16, 24, 32].includes(bytes.length)) {
throw new Error(`${name} must decode to 16, 24, or 32 bytes for AES.`);
}
return value;
}
requireServerActionsKey("NEXT_SERVER_ACTIONS_ENCRYPTION_KEY");
const nextConfig: NextConfig = {};
export default nextConfig;
Because next.config.ts is executed during the build, throwing there will fail CI immediately.
I would validate these at build time:
NEXT_SERVER_ACTIONS_ENCRYPTION_KEYDEPLOYMENT_VERSIONor another stable release identifier- any proxy-related origin values used in
serverActions.allowedOrigins
My recommendation
If I had to reduce this to a short checklist, it would be:
- set
NEXT_SERVER_ACTIONS_ENCRYPTION_KEYduringnext build; - ship the same build artifact to every instance;
- configure
deploymentId, andgenerateBuildIdif you rebuild in multiple places; - add
serverActions.allowedOriginsif a proxy hostname is involved; - block obviously malformed
next-actionrequests at the reverse proxy; - optionally add a small
proxy.tsguard inside Next.js too; - fail the build if the encryption key or deployment identifier is missing.
That combination covers both the official deployment-skew problem and the malformed-request problem reported in the GitHub discussion.