LEARN · DEBUGGING GUIDE

Next.js Middleware Not Executing on Expected Routes

Middleware silently skipping requests is a bigger pain than a hard error. Here's how to catch silent skips, misconfigurations, and file naming traps.

IntermediateNext.js4 min read

What this usually means

Nine times out of ten, this means your middleware file is mismatched with your routes, or your matcher config is too narrow, has typos, or is using unsupported patterns. Sometimes the root cause is a deployment or platform issue: Vercel edge runtimes can skip middleware silently if certain APIs, dependencies, or incompatible Next.js features are used. Less often, the culprit is missing permissions or a subtle bug in the exported function signature. Middleware that only runs in edge environments can be bypassed locally if you're not running with the right config.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Check for middleware.ts/middleware.js at the project root—use `find . -name 'middleware.*'` to confirm
  • 2Review matcher config in middleware for typos or unexpected regex patterns
  • 3Add `console.log` at the top of middleware and restart dev server with `npm run dev`
  • 4Request a known in-scope route directly and inspect response headers for expected middleware changes
  • 5Run `next build` and check output for 'Middleware' section and warnings
  • 6If deployed, inspect hosting platform logs (e.g., Vercel Edge Logs tab) for silent middleware execution or skips
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchmiddleware.ts or middleware.js at project root
  • searchmiddleware matcher property (inside middleware file)
  • searchnext.config.js for experimental or edgeRuntime config
  • search.next/server/middleware-manifest.json for effective matcher paths
  • searchDeployment logs (Vercel or Netlify dashboards)
  • searchBrowser DevTools → Network tab for response headers or missing rewrites
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningmiddleware file missing at project root (inside /pages or /app instead)
  • warningmatcher property doesn't include the intended route pattern (typo, missing slash, unsupported glob)
  • warningUsing dynamic parameters in matcher (unsupported until Next.js 13.2)
  • warningAccessing unsupported APIs or Node primitives in edge middleware (e.g., fs, process.env in some configs)
  • warningBuilding on unsupported Next.js version or with experimental flags disabled
  • warningHosting platform disables edge middleware due to plan or config restrictions
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildMove middleware.ts to project root (not /pages, /app, or /src)
  • buildAdjust matcher property to include all required routes, e.g. `matcher: ['/((?!api|_next/static|favicon.ico).*)']`
  • buildUpgrade Next.js to >=13.2 if you need dynamic parameters in matcher
  • buildRemove forbidden Node.js APIs from middleware—prefer Web APIs or primitives only
  • buildConfirm edgeRuntime and experimental flags in next.config.js as needed
  • buildClear build output (`rm -rf .next`) and redeploy after config changes
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedSee middleware console logs in terminal or hosting logs after a fresh request
  • verifiedObserve custom response headers set by middleware in browser DevTools
  • verifiedConfirm requests are rewritten or redirected per your middleware logic
  • verifiedTest with multiple routes (including wildcards) to check matcher coverage
  • verifiedRe-run `next build` and ensure no middleware warnings or errors appear
  • verifiedDeploy to staging and verify edge logs show execution
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningPlacing middleware.ts inside /pages or /src—Next.js will not detect it there
  • warningUsing advanced matcher patterns not supported by your Next.js version
  • warningLeaving console.log only in error branches (log on every execution to confirm running)
  • warningAssuming dev and prod match—double-check config and platform-specific behaviors
  • warningIgnoring case sensitivity in matcher and path names
  • warningRelying solely on build logs—always test with real HTTP requests
( 07 )War story

Middleware Silently Skipped Due to Matcher Typo in Next.js 13.1

Full-stack EngineerNext.js 13.1 on Vercel, TypeScript, Chrome DevTools

Timeline

  1. 10:01Merge deploys new auth middleware to Vercel
  2. 10:05QA reports login page not gating unauthenticated users
  3. 10:09Engineer tails Vercel logs; no middleware logs appear for /login
  4. 10:12Finds matcher: ['/dashboard/*'] in middleware.ts
  5. 10:16Realizes '/login' is not matched by '/dashboard/*'; updates matcher to ['/(.*)']
  6. 10:18Deploys fix, sees logs for /login in Edge logs
  7. 10:23QA confirms auth gating works on /login again

I shipped an authentication middleware to enforce login gating, using Next.js 13.1 on Vercel. The matcher property was set as ['/dashboard/*'], since most protected routes live under /dashboard.

QA immediately found that /login was unprotected and allowed unauthenticated access. I checked the Vercel Edge logs and noticed none of my console.log statements ever appeared for /login, even after multiple requests.

After re-reading the matcher docs, I realized '/dashboard/*' doesn't capture '/login' at all. I changed it to ['/(.*)'], deployed, and instantly saw my logs on /login. The fix was embarrassingly simple, but finding it required thinking about matcher syntax, not just code logic.

Root cause

Matcher property too narrow, excluding key routes from middleware execution.

The fix

Updated matcher to ['/(.*)'] so all routes—including /login—are matched by middleware.

The lesson

Middleware is opt-in per matcher. Always test every route you expect to be protected, and log on all invocations—not just errors.

( 08 )Hidden Middleware Placement Gotchas

Next.js only recognizes middleware.ts (or .js) located directly in the project root. Placing it in /pages, /src, or /app means it's completely skipped. Run `find . -name 'middleware.*'` to confirm location.

Subtle: If you have multiple folders (monorepo), or use a custom rootDir, Next.js might not pick up middleware where you expect. Always check your actual build output for the 'Middleware' manifest and verify which file is being loaded.

( 09 )Matcher Syntax and Globs: Pitfalls

Matchers are NOT full regex—Next.js uses its own pattern logic. For example, '/foo/*' only matches direct children, not deeper paths. For all routes, use ['/(.*)'] or the newer extended matchers (13.2+).

Unsupported features (like dynamic params before 13.2, or curly bracket syntax) simply skip middleware, often without an explicit warning. When in doubt, start with a very broad matcher and narrow down.

( 10 )Edge Runtime and Forbidden APIs

Middleware runs in the edge runtime—no access to Node.js built-ins like 'fs', 'os', or even most of 'process'. Accidentally importing these will cause silent failure or runtime errors that stop execution.

Use only standard Web APIs and primitives. If you need secrets or env vars, they must be defined using the 'NEXT_PUBLIC_' prefix or injected at build time. Private runtime config is not accessible.

( 11 )Platform-Specific Quirks: Dev vs Deploy

Dev mode (npm run dev) can behave differently than production (next build && next start) or serverless (Vercel/Netlify) edge. Verify actual middleware invocation in all environments—Vercel's Edge Logs are critical.

If you see inconsistent behavior between environments, check for differences in Next.js version, enabled experimental flags, or platform edge runtime support. Sometimes plans or project configs silently disable features.

Frequently asked questions

Can I use environment variables in middleware?

Only if they're public—i.e., prefixed with NEXT_PUBLIC_. Edge middleware can't access server-private env vars.

Why do some routes trigger middleware but not others?

99% of the time, it's a matcher config error. Use ['/(.*)'] to match all routes, then narrow as needed.

Does Next.js middleware run for API routes?

No. By default, API routes (under /pages/api/*) are excluded. Adjust matcher if you need to include them, but be careful.

Why does my middleware work locally but not after deploying?

Edge middleware can behave differently in cloud (Vercel/Netlify) than in dev. Check runtime logs, matcher patterns, and for forbidden APIs.