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.
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#
- In vercel/next.js issue 86241, the report says: "
Proxy file's negative matching is now ignored" after deploying to Vercel. - In vercel/next.js issue 86303, the production symptom was: "
proxyis not running" while local dev was fine. - In Stack Overflow 78713102, the error was: "
Module not found: Can't resolve 'dns'". - In Stack Overflow 78751647, the user described: "middleware fails to retrieve the token" on Vercel.
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, ortrailingSlashbehavior that only appears after deployment.
Use a deliberately boring matcher while debugging:
// 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.
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:
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.
// 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.
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:
NEXTAUTH_SECRETorAUTH_SECRETexists in Vercel production, not only preview or development.- The middleware uses the same secret name as the auth route.
- Your matcher excludes the auth callback and static assets.
- The auth middleware runs before your custom redirect wrapper if you compose middleware.
For NextAuth v4 style middleware:
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:
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:
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.
- 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-rewriteand your own temporary headers to identify the exact failing branch.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
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.
Continue Reading
Next.js Build Passed But Production Broke
A green build only proves the bundle compiled. This postmortem walks through three production-only failures: missing Vercel env vars, Edge runtime imports, and cache behavior that changed after deploy.
Fix next-auth /api/auth/error in production: NO_SECRET
Reproduced on next-auth 4.24.15 and Next.js 15.5.28: why sign-in works on localhost but fails after deploy, what a 404 versus a 500 at /api/auth/error tells you, and the env change that fixes it.
Fix /tmp/chromium libnss3.so Error on Vercel and Lambda
Get headless Chromium launching again in a Vercel Function or on AWS Lambda, and understand why pinning Node 14 was never a real fix.


