Middleware runs on every request by default — including static files (CSS, JS, images, fonts) — an unconfigured matcher causes Edge Runtime to execute on ALL assets
The matcher config is not optional — without it, your auth check runs on favicon.ico, _next/static/*, and every API route
Middleware blocks the request-response cycle — every millisecond spent in middleware adds to TTFB for every asset
Use matcher with regex patterns to restrict middleware to specific paths: matcher: ['/((?!api|_next/static|favicon.ico).*)']
Geolocation, rate limiting, A/B testing, and session refresh are valid middleware use cases — but only on the paths that need them
Missing matcher on a high-traffic site caused CPU to spike 12x — middleware ran on 95% of requests that were static assets
✦ Definition~90s read
What is Middleware in Next.js 16?
Next.js middleware is a function that runs on the Edge Runtime before every matching request. It can inspect and modify requests, rewrite URLs, redirect users, set headers and cookies, and implement geolocation-based routing. Middleware is defined in a single middleware.ts file at the root of the project.
★
Next.js middleware is like a security guard checking every single person entering a building — including delivery drivers, mail carriers, and people just walking past.
Middleware sits between the CDN and your application — it acts as a routing layer that can authenticate users, assign A/B test variants, localize content based on geography, and maintain sessions — all before the request reaches your page components.
The critical configuration is the matcher, which controls which request paths trigger middleware execution. Without a matcher, middleware runs on all requests including static assets, which multiplies execution cost and degrades performance.
Plain-English First
Next.js middleware is like a security guard checking every single person entering a building — including delivery drivers, mail carriers, and people just walking past. Most of those people just need to deliver packages (serve static files) and should not be checked. The matcher config tells the guard which doors to stand at — without it, the guard checks everyone, slowing down the entire building.
⚙ Browser compatibility
Latest versions — ✓ supported
Chrome
Firefox
Safari
Edge
✓
✓
✓
✓
Middleware in Next.js runs on the Edge Runtime before every request reaches your application. The default is 'every path' — including /_next/static/*, /favicon.ico, all CSS/JS chunks, images, and API routes. Without a matcher, your authentication middleware checks the session cookie on every single static asset request.
On a typical page load, the browser requests 30-80 static assets (CSS, JS, images, fonts). Without a matcher, your auth middleware runs 30-80 times per page view — once for the HTML, and 30-80 times for assets that never need authentication. This multiplies Edge function invocations, increases CPU usage, and adds latency to every asset delivery.
This article covers middleware matcher configuration, rewrite/redirect strategies, geolocation-based routing, session refresh patterns, and a production incident where a missing matcher caused 12x CPU usage on a high-traffic e-commerce site.
Tested on Next.js 16.0.0-canary. Middleware behavior is consistent across Next.js 12+.
Matcher Is Not Optional — The Default Will Destroy Your Performance
Next.js middleware runs on EVERY request by default. Not 'every route' — every request. CSS files, JS chunks, images, fonts, favicon.ico, robots.txt, sitemap.xml, API routes. All of them. The default matcher is '/:path*' — match everything.
The performance impact is multiplicative. A typical page load makes 50-100 requests for static assets. Without a matcher, your middleware runs 50-100 times per page view. Each middleware execution blocks the asset delivery — every millisecond in middleware adds 50-100ms to the total page load.
Edge Function pricing on Vercel: $2 per 1M invocations (or included in Pro plan). Without matcher: 12M invocations/day = $24/day in overage. With proper matcher: 1M invocations/day = included in plan. The cost savings alone justify the 30 seconds it takes to add matcher.
The matcher config accepts two formats
Array of path patterns: matcher: ['/dashboard/:path', '/account/:path']
Single regex string: matcher: '/((?!_next/static|favicon.ico).*)'
Use the array format when you know exactly which paths need middleware. Use the regex format when you want to exclude static assets while running on everything else.
middleware.ts — matcher patternsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// ============================================// Middleware Matcher Configuration// NEVER deploy middleware without a matcher// ============================================// BAD: No matcher — runs on EVERY request// export { default } from 'next-auth/middleware'export { default } from'next-auth/middleware'// BAD: Too broad matcher — only excludes _next/static// Still runs on API routes, images, fonts, etc.exportconst config = {
matcher: ['/((?!_next/static).*)']
}
// GOOD: Explicit protected paths can run middleware on routes that need itexportconst config = {
matcher: [
'/account/:path*',
'/checkout/:path*',
'/admin/:path*',
'/api/orders/:path*',
]
}
// BETTER: Exclude all static and public assets// This runs middleware on all routes EXCEPT listed pathsexportconst config = {
matcher: [
/*
* Match all request paths except:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - public directory files
* - api/auth (NextAuth.js API routes — handles its own auth)
*/
'/((?!api/auth|_next/static|_next/image|favicon.ico|images|fonts).*)',
]
}
// BEST: Use explicit path list when possible// Faster to evaluate and easier to auditexportconst config = {
matcher: [
'/account/:path*',
'/checkout/:path*',
'/admin/:path*',
'/api/protected/:path*',
]
}
Deploying middleware without matcher is a production incident waiting to happen. Add matcher in the SAME PR that creates middleware.ts. Not in a follow-up. The default 'match everything' is a performance trap.
Production Insight
e-commerce site deployed export { default } from 'next-auth/middleware' without matcher. 12M edge invocations/day instead of 1M. 15-minute fix saved $550/month.
Rule: every middleware.ts file must have an accompanying export const config = { matcher: [...] }.
Key Takeaway
Middleware runs on EVERY request by default — static assets, API routes, everything.
Always add matcher config: array format for explicit paths, regex format for broad exclusion.
Missing matcher causes 50-100x more middleware executions than needed per page load.
thecodeforge.io
Nextjs Middleware Complete Guide
Middleware Rewrites: BeforeFiles, AfterFiles, and Fallback Priority
Middleware rewrites are not the only rewrite mechanism in Next.js. next.config.js rewrites have three phases: beforeFiles, afterFiles, and fallback. Middleware rewrites apply between beforeFiles and afterFiles.
This ordering matters when using middleware for A/B testing or geolocation. If your middleware rewrites /products to /products?variant=B, but next.config.js has a beforeFiles rewrite that catches /products first, the middleware rewrite never runs.
For geolocation rewrites, add the rewrite logic in middleware and ensure next.config.js does not have a conflicting beforeFiles rule. Test the rewrite chain by adding console.log in middleware and checking the build output.
middleware.ts — rewrite with geolocationTYPESCRIPT
Middleware rewrites sit between beforeFiles and afterFiles. If you need middleware rewrites to take priority over dynamic routes, put exclusion rules in matcher. If you need beforeFiles priority, move the rewrite to next.config.js.
Production Insight
A team's geolocation middleware was not working because next.config.js had a beforeFiles rewrite for /products that caught the request before middleware ran.
Rule: check next.config.js rewrites before debugging middleware — they may intercept before middleware executes.
Key Takeaway
Middleware rewrites apply between beforeFiles and afterFiles — not at the top of the chain.
Use matcher to control which paths reach middleware — a broad matcher with internal path checks is inefficient.
Geolocation, A/B testing, and locale detection are valid middleware rewrite use cases.
Middleware Redirects vs next.config.js Redirects — Priority and Use Cases
next.config.js redirects have HIGHER priority than middleware redirects. If both define a redirect for the same path, next.config.js wins. Middleware redirects are evaluated during the request (dynamic), while next.config.js redirects are evaluated at build time (static).
Use next.config.js redirects for
Permanent URL changes (301 redirects that should be cached by search engines)
Domain migrations
Path restructurings that are stable and unchanging
Use middleware redirects for
Authentication-based redirects (redirect unauthenticated users to /login)
Feature flag redirects (redirect to maintenance page)
Temporary redirects that need access to request context (cookies, headers, geolocation) -Dynamic redirects that change based on request-time data
Performance note: next.config.js redirects are evaluated at the CDN/edge and do not incur Edge Function runtime. Middleware redirects run through the Edge Function and cost execution time. Whenever possible, use next.config.js for static redirects.
next.config.ts — static redirects vs middleware redirectsTYPESCRIPT
next.config.js redirects execute at the CDN level — no Edge Runtime invocation, no additional cost. Middleware redirects run through Edge Functions. Use next.config.js for any redirect that does not need request-time data (cookies, headers, geolocation).
Production Insight
A team put all redirects in middleware because they thought it was the canonical pattern. They paid Edge Function costs for 50+ redirects that could have been static next.config.js entries. Monthly cost: $200 vs $0.
Rule: if the redirect does not change based on request context, put it in next.config.js.
Key Takeaway
next.config.js redirects have higher priority than middleware redirects.
next.config.js redirects execute at CDN level (no Edge Runtime cost) — use for static redirects.
Middleware redirects use Edge Runtime — use for auth, feature flags, and request-time decisions.
Middleware with vs without MatcherPerformance and behavior comparison for auth checksWith MatcherWithout MatcherScope of ExecutionOnly protected routesAll routes including static filesCPU OverheadNormal (baseline)12x increase on static assetsAuth Check FrequencyPer request to protected routesPer request to every fileStatic File CachingUnaffectedBypassed or delayedConfiguration ComplexitySimple regex patternsNone required but dangerousRecommended UseAlways define matcherNever omit matcherTHECODEFORGE.IO
thecodeforge.io
Nextjs Middleware Complete Guide
Geolocation in Middleware: Country-Based Routing Without a Third-Party API
Next.js middleware provides geolocation data through the request.geo object. On Vercel, this includes country, city, region, latitude, and longitude — populated from the CDN edge node's IP geolocation database. No third-party API call needed, no additional latency.
The geo data is available in the middleware's Edge Runtime but NOT in Server Components or Route Handlers by default. If you need geo data in pages, pass it through headers or cookies in middleware.
Pattern: check geo.country in middleware, set a cookie with the locale, rewrite the URL with locale as a search param. Server Components read the cookie or search params to localize content.
Important: geolocation data is only available on platforms that provide it (Vercel, Cloudflare via request.cf). Self-hosted Next.js on plain Node.js does not populate geo data — you need to add your own IP geolocation or skip geo-dependent middleware for self-hosted deployments.
middleware.ts — geolocation to header patternTYPESCRIPT
Geo data is only available in middleware (Edge Runtime). To use it in Server Components, forward it via headers or cookies in middleware. Without this explicit forwarding, geo data is unavailable in page components.
Production Insight
A team deployed geo-personalized pricing using middleware headers. It worked on Vercel but broke on self-hosted — geo was always undefined because self-hosted Node.js does not provide IP geolocation.
Rule: always provide a fallback for geo data when deploying to non-Vercel environments.
Key Takeaway
Geolocation data is available in middleware via request.geo on Vercel/Cloudflare.
Forward geo data to Server Components via headers set in middleware.
Self-hosted environments do not provide geo data — always provide fallback values.
Session Refresh in Middleware: The Silent Performance Killer
Refreshing authentication sessions in middleware is a common pattern — check the session expiry, refresh via a token API, update the cookie. The problem: this happens on EVERY matching middleware invocation. If the session refresh involves a database lookup or an external API call, each middleware execution blocks for the duration of that call.
A session refresh that takes 200ms (db lookup + token rotation) on a matcher that runs on 50 paths per page view adds 10 seconds of blocking time to each page load. Worse: the Edge Runtime has limited connectivity — outbound HTTP requests from Edge Functions have higher latency and stricter timeouts than serverless functions.
The fix: defer session refresh to Route Handlers. Only check session presence in middleware (fast, <1ms cookie read). If the session needs refreshing, redirect to a Route Handler that performs the refresh and redirects back. This moves the 200ms operation out of the critical path.
Or use a short-lived access token with a long-lived refresh token pattern. The access token is validated in middleware (JWT decode, no DB call) and the refresh is done asynchronously via a Route Handler when the access token expires.
middleware.ts — session presence check, not refreshTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// ============================================// BAD: Session refresh in middleware// ============================================// DON'T DO THIS — blocks every matching requestexportasyncfunctionmiddleware(request: NextRequest) {
const token = request.cookies.get('session')
if (token) {
// This external call blocks every request for 200ms
const refreshed = await fetch('https://auth.example.com/refresh', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token.value}` }
})
if (refreshed.ok) {
const { newToken } = await refreshed.json()
const response = NextResponse.next()
response.cookies.set('session', newToken, { maxAge: 3600 })
return response
}
}
returnNextResponse.next()
}
// ============================================// GOOD: Session check only — refresh deferred// ============================================// middleware.tsexportasyncfunctionmiddleware(request: NextRequest) {
const { nextUrl } = request
// Only check protected pathsconst protectedPaths = ['/account', '/checkout', '/orders']
const isProtected = protectedPaths.some(
path => nextUrl.pathname.startsWith(path)
)
if (!isProtected) returnNextResponse.next()
const token = request.cookies.get('session')
if (!token) {
// No token at all — redirect to loginconst loginUrl = nextUrl.clone()
loginUrl.pathname = '/login'
loginUrl.searchParams.set('redirect', nextUrl.pathname)
returnNextResponse.redirect(loginUrl)
}
// Token exists — let the page handle validation/refresh// This is fast (<1ms) because we only read a cookiereturnNextResponse.next()
}
// app/account/page.tsx — handles actual session validationimport { refreshSession } from'@/lib/auth'exportdefaultasyncfunctionAccountPage() {
const session = awaitrefreshSession()
// This runs in Server Components — no Edge Runtime timeout issuesif (!session.isValid) {
// Handle expired/invalid sessionreturn <RedirectToLogin />
}
return <AccountContent session={session} />
}
Middleware runs on Edge Runtime with limited Node.js APIs. Database connections, filesystem access, and long external API calls are not available or perform poorly. Keep middleware to cookie/header inspection only — less than 5ms execution time.
Production Insight
A team's session refresh in middleware caused 5-second TTFB on protected pages. The refresh API call + DB update took 400ms, and on slow cellular connections it timed out entirely.
Rule: middleware should only check session PRESENCE (cookie exists), not session VALIDITY (token is fresh). Defer validity checks to Route Handlers or Server Components.
Key Takeaway
Keep middleware execution under 5ms — cookie/header reads only.
Defer session refresh to Route Handlers — middleware is not the place for external API calls.
Check session presence in middleware; validate and refresh in Server Components or Route Handlers.
Middleware and next.config.js Rewrite Order: The Full Chain
The complete rewrite/redirect chain in Next.js:
next.config.js — headers (set response headers before anything)
next.config.js — redirects (highest priority, evaluated by CDN)
next.config.js — rewrites (beforeFiles)
Middleware — runs on the request (can rewrite, redirect, set headers, set cookies)
Middlewares runs AFTER beforeFiles rewrites but BEFORE afterFiles rewrites. This means: - beforeFiles rewrites can intercept requests before middleware sees them - Middleware can rewrite URLs that then match afterFiles rewrites - fallback rewrites catch URLs that no route matched
Practical implications
If you need middleware to run on a rewritten URL, the middleware matcher must match the FINAL URL, not the original
If middleware rewrites a URL, the new URL goes through afterFiles rewrites but NOT beforeFiles rewrites again (no loop)
Static redirects in next.config.js (301) catch requests before middleware — middleware cannot override them
Debugging rewrite chainTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// ============================================// Testing the rewrite chain// ============================================// next.config.ts// Add logging to understand the rewrite chainconst nextConfig: NextConfig = {
asyncrewrites() {
return {
beforeFiles: [
// Stage 1: beforeFiles — runs before middleware
{
source: '/admin-old',
destination: '/admin',
},
],
afterFiles: [
// Stage 3: afterFiles — runs after middleware
{
source: '/products/:path*',
destination: '/catalog/:path*',
},
],
fallback: [
// Stage 5: fallback — no route matched
{
source: '/:path*',
destination: '/not-found',
},
],
}
},
}
// middleware.ts — runs between beforeFiles and afterFiles// If middleware rewrites /products to /catalog:// - The rewrite happens after any beforeFiles rewrite// - The new URL (/catalog) goes through afterFiles rewrites// - afterFiles can further rewrite /catalog to something elseexportfunctionmiddleware(request: NextRequest) {
const { nextUrl } = request
// This runs after beforeFiles, before afterFilesif (nextUrl.pathname.startsWith('/admin')) {
// Check admin access — redirect to login if not authorizedconst isAdmin = request.cookies.get('admin-token')
if (!isAdmin) {
returnNextResponse.redirect(newURL('/login', nextUrl))
}
}
returnNextResponse.next()
}
beforeFiles > Middleware > afterFiles > File System > fallback. If a rewrite is not working, check which stage it belongs to. Middleware rewrites cannot override beforeFiles, and afterFiles rewrites cannot undo middleware redirects.
Production Insight
A team's middleware rewrite to /products-v2 was being undone by an afterFiles rewrite back to /products. The two rewrites created an infinite redirect loop that crashed the edge function.
Rule: when combining middleware and next.config.js rewrites, trace the full chain for loops. A URL can be rewritten multiple times — each rewrite adds latency.
If you import a library in middleware.ts, verify it is Edge-compatible. Prisma, Mongoose, jsonwebtoken, bcrypt, and most ORMs do not work on Edge Runtime. Use Edge-compatible alternatives or move the logic to Route Handlers.
Production Insight
A team's middleware crashed on every request because they imported Prisma for a database session lookup. The error was a silent 500 on every page — no obvious stack trace because Edge Runtime errors are hard to catch.
Rule: if middleware fails silently, check for incompatible Node.js imports. Strip down to pure Web APIs first, then incrementally add complexity.
Key Takeaway
Middleware uses Edge Runtime — limited to Web APIs (fetch, crypto.subtle, URL).
No database connections, filesystem access, or Node.js modules.
Use edge-compatible libraries (jose for JWT, nanoid for IDs) or defer operations to Route Handlers.
● Production incidentPOST-MORTEMseverity: high
Auth Middleware Without Matcher — 12x Edge Function Cost, Pages Slowing Down
Symptom
Middleware was added in middleware.ts without a matcher config. The default behavior runs middleware on every request — including /_next/static/*.js, .css, .png, .webp, /favicon.ico, and all other static assets. The middleware's session cookie check and database lookup ran 80+ times per page load, blocking every static file delivery.
Root cause
The developer who added middleware.ts did not include a matcher configuration. Next.js middleware documentation shows examples WITHOUT matcher in quick-start sections, leading developers to believe it is optional. It is not optional for production — without matcher, middleware runs on every edge request including all static assets.
Fix
Added matcher to middleware.ts: export const config = { matcher: ['/account/:path', '/checkout/:path', '/api/auth/:path*'] }. Edge function invocations dropped from 12M/day to 800K/day — a 93% reduction. Page load time returned to 800ms. LCP returned to 1.8s. Cloud bill dropped from $600/month to $45/month.
Key lesson
Matcher is not optional — always specify which paths middleware should run on
Default behavior runs middleware on ALL requests including static assets — this is almost never what you want
Before matcher fix: 80 middleware calls per page view. After: 2-3 calls per page view
Always check Cloudflare/Edge function metrics after deploying middleware — a sudden spike indicates missing matcher
Document matcher patterns in your team's Next.js conventions — this mistake is too easy to make
Production debug guideDiagnose slow middleware, over-invocation, and configuration issues4 entries
Symptom · 01
Edge function invocations spiked after adding middleware
→
Fix
Check if matcher config is missing or too broad. Middleware without matcher runs on every request. Add matcher to restrict paths. Also check for too many middleware files — only one middleware.ts is supported per project.
Symptom · 02
Authentication middleware runs on /_next/static files
→
Fix
Matcher regex needs to exclude static paths. Use matcher: ['/((?!api|_next/static|favicon.ico).*)'] to exclude. Or explicitly list protected paths instead of using a negative lookahead.
Symptom · 03
Middleware rewrite/redirect not working as expected
→
Fix
Middleware redirects have lower priority than next.config.js redirects. Check if next.config.js redirects or rewrites are intercepting before middleware runs. Also check that the middleware is running on the correct path — matcher may exclude the target URL.
Symptom · 04
Session refresh in middleware causes slow responses
→
Fix
Middleware runs on Edge Runtime with limited Node.js APIs. Avoid heavy computation, database queries, or external API calls in middleware. Use simple cookie/token validation and defer heavy operations to Route Handlers.
★ Middleware Quick Debug ReferenceFast commands and patterns for middleware debugging
Use 'beforeFiles: []' in next.config.js rewrites for higher priority
⚙ Quick Reference
7 commands from this guide
File
Command / Code
Purpose
middleware.ts — matcher patterns
export { default } from 'next-auth/middleware'
Matcher Is Not Optional
middleware.ts — rewrite with geolocation
export function middleware(request: NextRequest) {
Middleware Rewrites
next.config.ts — static redirects vs middleware redirects
const nextConfig: NextConfig = {
Middleware Redirects vs next.config.js Redirects
middleware.ts — geolocation to header pattern
export function middleware(request: NextRequest) {
Geolocation in Middleware
middleware.ts — session presence check, not refresh
export async function middleware(request: NextRequest) {
Session Refresh in Middleware
Debugging rewrite chain
const nextConfig: NextConfig = {
Middleware and next.config.js Rewrite Order
Edge-compatible vs Node.js APIs in middleware
export async function middleware(request: NextRequest) {
Middleware Limitations
Key takeaways
1
Middleware runs on EVERY request by default
always add an explicit matcher config to restrict paths
2
Missing matcher causes 50-100x more middleware executions per page load
12M vs 800K invocations/day
3
Static redirects belong in next.config.js
middleware redirects are for auth, geolocation, and dynamic decisions
4
Middleware uses Edge Runtime
no database, no filesystem, no Node.js modules
5
Keep middleware under 5ms
cookie/header reads only, defer session refresh to Route Handlers
6
Geolocation is middleware-only
forward to Server Components via headers or cookies
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
What is the default scope of Next.js middleware and why is it dangerous?
Q02SENIOR
Explain the rewrite/redirect priority chain in Next.js. Where does middl...
Q03SENIOR
What API limitations does the Edge Runtime have in middleware?
Q04SENIOR
How do you handle geolocation-based routing in Next.js middleware?
Q01 of 04SENIOR
What is the default scope of Next.js middleware and why is it dangerous?
ANSWER
Next.js middleware runs on EVERY request by default — 'matches all paths'. This includes static assets like CSS files, JavaScript bundles, images, fonts, and favicon.ico. The danger is that without an explicit matcher configuration, your middleware (which may contain auth checks, geolocation lookups, or session validation) executes 50-100 times per page view instead of 1-2 times.
This manifests as: 12x higher Edge Function invocations, 40% slower page loads (because every static file is blocked by middleware execution), and significantly higher cloud bills ($50/month becoming $600/month in one documented production incident).
The fix is always adding a matcher config that either explicitly lists protected paths or uses a regex to exclude static assets. The matcher is not optional — it is a performance-critical configuration that should be added in the same commit as the middleware itself.
For production applications, the recommended approach is explicit path lists: matcher: ['/dashboard/:path', '/account/:path']. For broad exclusion: matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'].
Q02 of 04SENIOR
Explain the rewrite/redirect priority chain in Next.js. Where does middleware fit?
ANSWER
The complete priority chain from highest to lowest is:
1. next.config.js headers — set response headers before any processing
2. next.config.js redirects — permanent/temporary redirects evaluated at the CDN level
3. next.config.js beforeFiles rewrites — rewrite URLs before middleware runs
4. Middleware — runs on Edge Runtime, can rewrite, redirect, set headers/cookies
5. next.config.js afterFiles rewrites — rewrite URLs after middleware but before file-system matching
6. File-system routing — matches page.tsx, layout.tsx, route.ts based on the (potentially rewritten) URL
7. next.config.js fallback rewrites — catch URLs that did not match any file-system route
Middleware sits between beforeFiles and afterFiles. This means:
- beforeFiles rewrites intercept requests before middleware sees them — middleware cannot undo them
- Middleware rewrites can be further transformed by afterFiles rewrites
- Fallback rewrites only apply if no file-system route matches the final URL
Understanding this chain is critical when troubleshooting 'my middleware rewrite is not working' — the issue is often a beforeFiles rewrite that caught the request first, or an afterFiles rewrite that transforms the URL further.
Q03 of 04SENIOR
What API limitations does the Edge Runtime have in middleware?
ANSWER
Middleware runs on Edge Runtime (based on V8 isolates, not Node.js), which has significant API limitations:
- No direct database access: Prisma, pg, mongoose, and most ORMs use Node.js TCP/Unix sockets that are unavailable. Use edge-compatible KV stores (Vercel KV, Cloudflare KV) or defer to Route Handlers.
- No filesystem: fs/promises, readFile, writeFile — none available.
- Limited Node.js modules: crypto, path, os, stream, http, https are not available. Use Web Crypto API (crypto.subtle), URL, URLSearchParams, and fetch instead.
- No async local storage: AsyncLocalStorage is Node.js-only.
- Timeout limits: Edge Functions have a 25-second timeout, but middleware should complete in under 10ms to avoid slowing down every request.
- No server-only Node modules: Any library that imports Node.js internals will throw an error.
Workaround: use jose for JWT (Web Crypto API compatible), nanoid for IDs, and the built-in Web APIs. Keep middleware logic minimal — cookie reads, header inspection, URL rewriting. Move computation-heavy or I/O-heavy operations to Route Handlers or Server Components.
Q04 of 04SENIOR
How do you handle geolocation-based routing in Next.js middleware?
ANSWER
Geolocation data is available in middleware via request.geo, which is populated by the CDN edge (Vercel, Cloudflare). It includes country, city, region, latitude, and longitude. This data is NOT automatically forwarded to Server Components or Route Handlers — it must be explicitly passed via headers, cookies, or search params.
The pattern:
1. In middleware, read geo.country
2. Determine the appropriate locale or variant based on country
3. Either rewrite the URL (adding locale as search param) or set response headers/cookies
4. In Server Components, read the header/cookie/searchParam to localize content
Important considerations:
- Geo data is only available on platforms that provide it (Vercel, Cloudflare). Self-hosted deployments return undefined for geo. Always provide fallback values.
- geo data is based on IP geolocation — it is approximate and may be incorrect for VPN users or corporate networks.
- Do not use geo data for access control (geoblocking) — it is trivially bypassable and inconsistently reliable.
- Cache geo-dependent responses appropriately — vary by x-geo-country header to maintain CDM cache efficiency.
01
What is the default scope of Next.js middleware and why is it dangerous?
SENIOR
02
Explain the rewrite/redirect priority chain in Next.js. Where does middleware fit?
SENIOR
03
What API limitations does the Edge Runtime have in middleware?
SENIOR
04
How do you handle geolocation-based routing in Next.js middleware?
SENIOR
FAQ · 7 QUESTIONS
Frequently Asked Questions
01
What happens if I deploy middleware without a matcher?
Middleware runs on EVERY request — including static assets like CSS, JS, images, fonts, and favicon.ico. A typical page load makes 50-100 static asset requests, so your middleware executes 50-100 times per page view instead of 1-2 times. This multiplies Edge Function invocations, increases CPU usage, and adds latency to every asset delivery. Always add a matcher config to restrict middleware to specific paths.
Was this helpful?
02
Can I have multiple middleware files in Next.js?
No — Next.js supports only one middleware.ts file at the root of your project (in the src/ or project root, same level as app/). If you need different middleware logic for different paths, combine them in a single middleware.ts with conditional path checks using the request.nextUrl.pathname.
Was this helpful?
03
What is the difference between next.config.js rewrites and middleware rewrites?
next.config.js rewrites have three phases with different priorities: beforeFiles (highest), afterFiles (medium), and fallback (lowest). They are configured at build time and evaluated at the CDN level without Edge Function invocation. Middleware rewrites run between beforeFiles and afterFiles, execute on Edge Runtime, and can access request-time data like cookies, headers, and geolocation. Use next.config.js for URL normalization and static rewrites. Use middleware for dynamic rewrites based on auth state, geolocation, or A/B tests.
Was this helpful?
04
How do I debug middleware not running on a specific path?
Check the matcher config first — the path may be excluded by the pattern. Next, check next.config.js beforeFiles rewrites which run before middleware and may redirect the request before middleware executes. Add a temporary console.log at the top of middleware to confirm it runs. Use 'next dev' with NODE_ENV=development for full error traces. If middleware throws an error silently, check for incompatible Node.js imports that fail on Edge Runtime.
Was this helpful?
05
Can middleware access the database?
No — middleware runs on Edge Runtime which does not support Node.js TCP/Unix sockets required by database drivers (Prisma, pg, mongoose, etc.). Do not attempt database access in middleware. If you need to look up data for routing decisions, either preload the data into an edge-compatible KV store (Vercel KV, Cloudflare KV) or defer the routing decision to a Route Handler or Server Component.
Was this helpful?
06
Is middleware executed on every navigation in a client-side app?
No — middleware only runs on SERVER-SIDE requests. Client-side navigations (using Link or router.push) that stay within the same application do NOT trigger middleware. The middleware response is cached by the client router for the navigation. However, full page loads, browser refreshes, and direct URL access always trigger middleware. This means middleware-protected routes that are navigated to via client-side Links may skip the middleware check — the page component itself must also verify authentication.
Was this helpful?
07
How do I use environment variables in middleware?
Environment variables are available in middleware, but only those prefixed with NEXT_PUBLIC_ are available in the client bundle. For middleware (which runs on Edge Runtime), you can use any environment variable without the prefix. Access them via process.env.VARIABLE_NAME. Note that middleware is bundled separately — environment variables are inlined at build time, not read at runtime. Use next.config.ts env or runtime environment variables for dynamic values.