March 14, 2026

How to fix "Failed to find Server Action" in Next.js

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:

  1. a real deployment skew problem;
  2. malformed requests hitting your app with garbage next-action headers.

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 deploymentId to 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 has and missing header 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_KEY
  • DEPLOYMENT_VERSION or 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:

  1. set NEXT_SERVER_ACTIONS_ENCRYPTION_KEY during next build;
  2. ship the same build artifact to every instance;
  3. configure deploymentId, and generateBuildId if you rebuild in multiple places;
  4. add serverActions.allowedOrigins if a proxy hostname is involved;
  5. block obviously malformed next-action requests at the reverse proxy;
  6. optionally add a small proxy.ts guard inside Next.js too;
  7. 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.

Sources