
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 how to disable Turbopack for a production build, 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. Remove Node-Only Imports From Middleware#
Middleware runs on the Edge Runtime. Anything that pulls in fs, net, tls, dns, many Redis clients, some database adapters, or native password libraries can break even if the import is behind a conditional branch.
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.
Move Node-only checks into a route handler, server action, or server component running in the Node.js runtime. Let middleware do only cheap checks: cookie presence, JWT verification with an Edge-safe library, pathname decisions, and rewrites.
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.
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 middleware, move that check out of middleware.
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.
Related#
One email a month — no fluff
RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.
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 Module not found: Can't resolve 'encoding' in Vercel
You see "Module not found: Can't resolve 'encoding'" in your Vercel deployment logs. This guide explains why it happens with cross-fetch/node-fetch and how to fix it permanently.
How to Access Route Parameter Inside getServerSideProps
Learn why your dynamic route parameter appears as undefined in getServerSideProps and how to correctly extract it from the context object for server-side data fetching.
Browse by Topic
Find stories that matter to you.
