afterbuild/ops
ERR-754/Next.js · Middleware
ERR-754
middleware.ts never runs — request goes directly to the route handler

appears when:When middleware.ts is in the wrong location, the matcher excludes the URL, or the project uses output: 'export' which disables middleware

Next.js middleware not running in production

Next.js ships middleware silently. Wrong location, wrong matcher, or static export and the file is skipped with no warning. The fix is always one of these three.

Last updated 17 April 2026 · 7 min read · By Hyder Shah
Direct answer
Middleware runs only if three conditions hold: the file is at the required location (root or src/), the config.matcher pattern matches the request URL, and the project is not in static export mode. Verify all three before editing middleware logic — most non-running middleware is a location or matcher problem, not a code problem.

Quick fix for Next.js middleware not running

middleware.ts
typescript
01// middleware.ts — place at project root or src/ root02import { NextResponse, type NextRequest } from "next/server";03 04export function middleware(req: NextRequest) {05  // Example: gate /dashboard behind a session cookie06  const session = req.cookies.get("session");07  if (!session && req.nextUrl.pathname.startsWith("/dashboard")) {08    const url = req.nextUrl.clone();09    url.pathname = "/login";10    url.searchParams.set("from", req.nextUrl.pathname);11    return NextResponse.redirect(url);12  }13  return NextResponse.next();14}15 16// Matcher uses regex, not Next router params.17// This pattern excludes API routes, static files, and _next internals.18export const config = {19  matcher: [20    "/((?!api|_next/static|_next/image|favicon.ico).*)",21  ],22};
Canonical middleware with a matcher that excludes internals but covers every page route

Deeper fixes when the quick fix fails

01 · Matcher with has / missing modifiers for header-based routing

middleware.ts
typescript
01// Match only authenticated requests (cookie set) on protected paths02export const config = {03  matcher: [04    {05      source: "/dashboard/:path*",06      has: [{ type: "cookie", key: "session" }],07    },08    {09      source: "/dashboard/:path*",10      missing: [{ type: "cookie", key: "session" }],11    },12  ],13};14 15// Pair with logic that branches on cookie presence — cleaner than16// checking cookies inside a single matcher that runs for both cases.
Has/missing modifiers filter by header or cookie before middleware runs — cheaper than in-function checks

02 · Edge-safe JWT verification with jose

middleware.ts
typescript
01// middleware.ts — Edge-compatible JWT check02import { jwtVerify } from "jose";03import { NextResponse, type NextRequest } from "next/server";04 05const SECRET = new TextEncoder().encode(process.env.JWT_SECRET!);06 07export async function middleware(req: NextRequest) {08  const token = req.cookies.get("session")?.value;09  if (!token) return NextResponse.redirect(new URL("/login", req.url));10 11  try {12    await jwtVerify(token, SECRET);13    return NextResponse.next();14  } catch {15    return NextResponse.redirect(new URL("/login", req.url));16  }17}18 19export const config = { matcher: "/dashboard/:path*" };
jose uses Web Crypto — works in Edge where jsonwebtoken does not

03 · Matcher regression test

tests/middleware-matcher.test.ts
typescript
01// tests/middleware-matcher.test.ts02import { describe, it, expect } from "vitest";03 04const MATCHER = new RegExp("^/((?!api|_next/static|_next/image|favicon.ico).*)$");05 06describe("middleware matcher", () => {07  it.each([08    ["/", true],09    ["/dashboard", true],10    ["/dashboard/settings", true],11    ["/api/user", false],12    ["/_next/static/abc.js", false],13    ["/_next/image?url=x", false],14    ["/favicon.ico", false],15  ])("matches %s -> %s", (url, expected) => {16    expect(MATCHER.test(url)).toBe(expected);17  });18});
Treat the matcher like any other regex — unit-test it so typos fail CI, not prod

Why AI-built apps hit Next.js middleware not running

Next.js middleware has a rigid contract: one file at a specific location, one exported function named middleware, and an optional configexport with matcher rules. Any deviation and Next.js silently skips the file without a build warning. The silence is the hardest part — there is no log line saying "your middleware is not running", just a request that flows past your auth gate into the route handler unhindered.

Location is the first trap. If your project has a src/ directory (most do in 2026), middleware lives at src/middleware.ts. If not, it lives at the project root alongside package.json. Any other path — inside app/, inside lib/, inside a subdirectory of src/ — is ignored. AI scaffolds frequently put middleware next to the routes it guards, which feels right but is wrong. A move-up to the canonical location is often the only fix needed.

The matcher is the second trap. Matchers use a regex subset, not the Next.js route params syntax. Writing matcher: '/blog/[slug]' does not work — you need /blog/:path* or /blog/(.*). The negative lookahead pattern to skip internals is fiddly: /((?!api|_next/static|_next/image|favicon.ico).*). A missing paren or a typo in _next silently drops matches for every URL. Test matchers with real paths: the Next.js documentation has a matcher playground.

The third trap is static export. When next.config.ts contains output: 'export', the build produces pure static HTML and JS with no server runtime. Middleware requires the Edge runtime to intercept requests. In static export mode, middleware is stripped out of the build silently. Vercel still deploys the app successfully — just without middleware. If auth stops working after a recent next.config change, check for static export first.

The fourth trap, less common, is importing a Node-only library in middleware. Things like jsonwebtoken that use Node crypto instead of Web Crypto will error at runtime on Vercel's Edge, taking the whole middleware down. Swap to Edge-compatible alternatives like jose.

Next.js middleware not running by AI builder

How often each AI builder ships this error and the pattern that produces it.

AI builder × Next.js middleware not running
BuilderFrequencyPattern
LovableEvery middleware scaffoldPlaces middleware.ts inside app/ instead of at root
Bolt.newCommonUses route-param syntax in matcher instead of regex
CursorCommonImports jsonwebtoken in middleware — fails in Edge runtime
Base44SometimesAdds output: 'export' to reduce deploy time, unaware it kills middleware
Replit AgentRareCreates multiple middleware.ts files in subdirectories, only root runs

Related errors we fix

Stop Next.js middleware not running recurring in AI-built apps

Still stuck with Next.js middleware not running?

Emergency triage · $299 · 48h turnaround
We restore service and write the root-cause report.
start the triage →

Next.js middleware not running questions

Where exactly should middleware.ts live in a Next.js project?+
If you use a src/ directory, middleware.ts must live at src/middleware.ts. If you do not use src/, it must live at the project root (next to package.json) as middleware.ts. Any other location is ignored silently — no warning in the build, no error at runtime. The file next.config.ts does not let you override the location. If you are not sure whether your project uses src/, check where the app/ directory is.
Why does my middleware work in dev but not on Vercel?+
The most common cause is a next.config file with output: 'export'. Static exports have no server runtime — middleware requires the Edge runtime and is silently disabled. The second cause is a matcher that excludes your production URLs. Matchers use a special syntax: /((?!api|_next/static|_next/image).*) is the common pattern, but typos in the negative lookahead will accidentally exclude everything. Test matchers with a real URL: the Next.js docs provide a matcher checker.
Can middleware call Node.js APIs like fs or crypto?+
Mostly no. Middleware runs on the Edge runtime, which is a subset of Node — no fs, no net, no child_process, no native modules. Web-standard APIs are available: fetch, Request, Response, URL, crypto.subtle. If your middleware imports a library that pulls in a Node-only module, the build logs a warning and the middleware may fail to run in production. Keep middleware logic thin: read cookies, check auth, rewrite or redirect. Heavier work belongs in API routes.
Why does the matcher config not work with dynamic routes?+
Matcher patterns are a subset of regex with specific quirks. They do not expand Next.js [param] placeholders — use regex instead. For /blog/[slug], match with source: '/blog/:path*' or '/blog/(.*)'. The has and missing modifiers let you match based on headers or query strings. Most matcher bugs come from trying to write next-router syntax in the matcher, which silently fails.
How long does an Afterbuild Labs middleware audit take?+
For a single middleware.ts with an auth check, diagnosis and fix is under 1 hour. Multi-matcher setups with rewrites, A/B tests, or regional routing run 1-3 hours. Our Integration Fix service includes middleware review, a matcher test harness that asserts which URLs are intercepted, and Edge-runtime compatibility analysis for any libraries the middleware imports.
Next step

Ship the fix. Keep the fix.

Emergency Triage restores service in 48 hours. Break the Fix Loop rebuilds CI so this error cannot ship again.

About the author

Hyder Shah leads Afterbuild Labs, shipping production rescues for apps built in Lovable, Bolt.new, Cursor, Replit, v0, and Base44. our rescue methodology.

Sources