Next.js

Next.js Middleware Not Running on Vercel: 3 Causes

Middleware that works locally can disappear in Vercel production because the matcher never matches, Edge bundles a Node-only import, or auth middleware cannot read the token. This guide gives you the diagnosis order I use before touching app code.

Mahdi Ben RhoumaMahdi Ben Rhouma
Updated
8 min read

Introduction#

If Next.js middleware works locally but not on Vercel, debug the matcher first, then the Edge Runtime bundle, then your auth token configuration. Most production failures are not "middleware is broken"; they are "the request never matched" or "the Edge function could not safely run the code you imported."

If you are also fighting build differences, read the Next.js Turbopack stuck / disable guide, why Vercel env vars go missing, and the larger Next.js middleware patterns guide.

Real Reports This Fix Is Based On#

Those are four different symptoms. The fix is to separate routing, runtime, and auth instead of changing all three at once.

1. Prove The Matcher Actually Includes The URL#

The most common failure is a matcher that looks right in review but does not match the deployed URL. In Next.js 15 and 16, the file convention may be middleware.ts in older apps or proxy.ts in newer migrations, but the matcher rule still has to start with /.

Bad matchers usually fail in one of three ways:

  • They omit the leading slash.
  • They try to negate too much in one regex.
  • They ignore locale, basePath, or trailingSlash behavior that only appears after deployment.

Use a deliberately boring matcher while debugging:

ts
// middleware.ts or proxy.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
 
export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  response.headers.set("x-debug-middleware", request.nextUrl.pathname);
  return response;
}
 
export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)"],
};

Deploy that. Hit the exact production URL. Inspect the response headers and Vercel function logs. If x-debug-middleware is missing, your real middleware logic is irrelevant because the request never entered the function.

Don't
matcher: ['((?!api|_next).*)']
Do
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
The Fix

The fix in one line: start with one leading-slash matcher, deploy, and prove the response gets an x-debug-middleware header before debugging auth.

2. Split Negative Matching From Auth Redirects#

Negative matching is fragile because a single missed path can turn an auth redirect into a loop. The GitHub Vercel report above reproduced a production-only case where negative matching was ignored with i18n configured. That does not mean every app has that bug, but it does mean you should make the redirect logic defensive.

Do not let the middleware redirect its own login page:

ts
const PUBLIC_PATHS = ["/login", "/signup", "/api/health"];
 
export function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname;
 
  if (PUBLIC_PATHS.some((path) => pathname === path || pathname.startsWith(`${path}/`))) {
    return NextResponse.next();
  }
 
  const token = request.cookies.get("session")?.value;
 
  if (!token) {
    const loginUrl = new URL("/login", request.url);
    loginUrl.searchParams.set("next", pathname);
    return NextResponse.redirect(loginUrl);
  }
 
  return NextResponse.next();
}

Keep the matcher broad and put the human-readable allowlist in code. That gives you a place to log pathname, next, and the redirect target without trying to mentally evaluate a regex during an incident.

3. Check Node-Only Imports When Middleware Uses Edge#

For Middleware configured to use Edge, importing fs, net, tls, dns, a Node-only Redis client, database adapter or native password library can break the bundle even behind a conditional branch. This restriction is runtime-specific: the Next.js 15 reference also documents a Node.js Middleware runtime, while Proxy uses Node.js.

This is the trap: bundlers analyze imports before your if statement runs.

ts
// Do not do this in middleware.
import Redis from "ioredis";
 
export async function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith("/admin")) {
    const redis = new Redis(process.env.REDIS_URL);
    // ...
  }
}

The Edge bundle still sees ioredis, and ioredis pulls Node modules. That is how you end up with the dns module error from the Stack Overflow report.

When keeping Middleware on Edge, move Node-only checks into a Route Handler, Server Action or Server Component running on Node.js. Alternatively, use the supported Node.js Middleware runtime for your deployment. Keep request-time work bounded: cookie presence, JWT verification, pathname decisions and rewrites should not add unnecessary latency to every matching request.

ts
import { jwtVerify } from "jose";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
 
const secret = new TextEncoder().encode(process.env.AUTH_SECRET);
 
export async function middleware(request: NextRequest) {
  const token = request.cookies.get("session")?.value;
 
  if (!token) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
 
  try {
    await jwtVerify(token, secret);
    return NextResponse.next();
  } catch {
    return NextResponse.redirect(new URL("/login", request.url));
  }
}

4. Check NextAuth Secret And Middleware Ordering#

NextAuth/Auth.js failures often look like "middleware not running" because the only visible symptom is an endless login redirect. The middleware is running; it just cannot decode the same token your auth route created. When the secret is missing altogether, next-auth v4 refuses to run in production and signIn() sends the browser to /api/auth/error, which the next-auth NO_SECRET production fix resolves by adding NEXTAUTH_SECRET to the Production environment and redeploying.

Verify these in order:

  1. NEXTAUTH_SECRET or AUTH_SECRET exists in Vercel production, not only preview or development.
  2. The middleware uses the same secret name as the auth route.
  3. Your matcher excludes the auth callback and static assets.
  4. The auth middleware runs before your custom redirect wrapper if you compose middleware.

For NextAuth v4 style middleware:

ts
export { default } from "next-auth/middleware";
 
export const config = {
  matcher: ["/dashboard/:path*", "/settings/:path*"],
};

For custom logic, decode once and log the result without leaking token contents:

ts
import { getToken } from "next-auth/jwt";
import { NextResponse } from "next/server";
 
export async function middleware(request) {
  const token = await getToken({
    req: request,
    secret: process.env.NEXTAUTH_SECRET,
  });
 
  console.log("middleware-auth", {
    path: request.nextUrl.pathname,
    hasToken: Boolean(token),
  });
 
  if (!token) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
 
  return NextResponse.next();
}

If hasToken is always false in production but true locally, stop editing redirects and fix environment variables or cookie domain settings.

5. Debug With Response Headers, Not Console Hope#

Vercel logs are useful, but response headers are faster during an incident. Add one temporary header at each branch:

ts
const response = NextResponse.next();
response.headers.set("x-middleware-branch", "authenticated");
return response;

For rewrites, check Vercel's request details and response headers for x-middleware-rewrite. If you expected a rewrite but the header is absent, the branch did not execute. If the header points to the wrong URL, your middleware ran and your rewrite target is wrong.

Do not leave sensitive headers in production forever. Keep harmless branch names, deploy, test, and remove them after the fix.

When this won't work
  • If you are on Next.js 12 or early 13, middleware file conventions and runtime behavior differ.
  • If you use a custom server, Vercel Edge middleware behavior is not the source of truth.
  • If your auth depends on a Node-only database adapter inside Edge Middleware, move that check to Node.js or use a supported Node.js Middleware runtime.

Summary#

  • First prove the deployed URL matches middleware with a temporary response header.
  • Keep the matcher broad and put fragile public-path exceptions in code.
  • Remove Node-only imports from middleware, even conditional imports.
  • For NextAuth/Auth.js, verify the production secret and cookie domain before changing redirects.
  • Use x-middleware-rewrite and your own temporary headers to identify the exact failing branch.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.