Next.js Middleware and the Edge Runtime
What Next.js middleware actually is, how it runs on the Edge, what you can and cannot do there, and the patterns that work in production.
What you'll learn
- ✓How Next.js middleware fits in the request lifecycle
- ✓What the Edge runtime supports and excludes
- ✓How to do auth, redirects, and AB tests in middleware
- ✓Performance traps to avoid
- ✓When to skip middleware entirely
Prerequisites
- •Familiarity with Next.js App Router
Middleware in Next.js is code that runs before a request reaches a route. It runs on the Edge runtime, which is a stripped-down V8 environment deployed close to users. Powerful, but constrained, and easy to misuse. This post is about what middleware is really for and how to keep it fast and safe.
What middleware actually is
A single middleware.ts file at the project root exports a function that receives the request and returns a Response (or nothing, meaning “continue”). It runs on every matched request, before pages or route handlers.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(req: NextRequest) {
const token = req.cookies.get('session')?.value;
if (!token && req.nextUrl.pathname.startsWith('/dashboard')) {
const url = req.nextUrl.clone();
url.pathname = '/login';
return NextResponse.redirect(url);
}
}
export const config = {
matcher: ['/dashboard/:path*'],
};
Use matcher to scope it. A middleware that runs on every request, including static assets, is one of the easiest ways to slow your whole site down.
The Edge runtime in one paragraph
Edge functions are not Node. There is no fs, no child_process, no native modules. The standard library is a subset of Web APIs: fetch, Request, Response, URL, crypto.subtle, TextEncoder. Cold starts are tiny (under 50 ms) and execution is metered by CPU time, often capped at 50 ms per request. That budget is your guardrail.
Mental model
Client --> CDN edge --> middleware (Edge runtime)
|
+---------------+----------------+
v v
rewrite / redirect continue to route
|
v
Edge route OR Node server route Middleware can rewrite, redirect, set cookies and headers, or short-circuit with its own response. It cannot read the request body and then forward it; it is meant to be a lightweight gate.
Hands-on: feature flags and AB test bucketing
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(req: NextRequest) {
const res = NextResponse.next();
let bucket = req.cookies.get('ab')?.value;
if (!bucket) {
bucket = Math.random() < 0.5 ? 'A' : 'B';
res.cookies.set('ab', bucket, { path: '/', maxAge: 60 * 60 * 24 * 30 });
}
res.headers.set('x-ab-bucket', bucket);
return res;
}
export const config = { matcher: ['/((?!_next|api|favicon.ico).*)'] };
The downstream React Server Component reads the cookie or header and serves the right variant. Bucketing in middleware avoids a layout shift from client-side AB.
Hands-on: auth gate with JWT verification
JWT verification works on the Edge using jose, which uses crypto.subtle. Do not use jsonwebtoken; it depends on Node crypto.
import { jwtVerify } from 'jose';
import { NextResponse } from 'next/server';
const secret = new TextEncoder().encode(process.env.JWT_SECRET!);
export async function middleware(req) {
const token = req.cookies.get('session')?.value;
if (!token) return NextResponse.redirect(new URL('/login', req.url));
try {
await jwtVerify(token, secret);
} catch {
return NextResponse.redirect(new URL('/login', req.url));
}
}
export const config = { matcher: ['/app/:path*'] };
Verify; do not authorize. Fine-grained checks (“can user X read project Y?”) belong in route handlers where you have a database connection.
Common pitfalls
- Matching
/with no exclusions. Every static asset hits middleware and you pay edge invocations for nothing. - Importing Node-only packages. Build will fail or, worse, succeed and fail at runtime on Vercel.
- Doing a database round-trip in middleware. Most DBs are not edge-friendly; latency stacks up across regions.
- Reading the request body. Middleware does not have body access for performance reasons; do that work in a route handler.
- Setting cookies on a
NextResponse.next()and assuming the downstream route sees them on the same request. It sees them only on subsequent requests. - Long await chains. Each
await fetchadds to your 50 ms budget. Parallelize withPromise.allor push the work elsewhere.
Practical tips
- Treat middleware as an L7 router with auth. If logic gets complex, it belongs in a route.
- Use
matcheraggressively. Exclude_next,api, images, and anything that does not need the gate. - Cache verification results in a signed cookie so you do not re-decode JWTs every request.
- Log timings.
Date.now()at entry and exit, send the delta in a header for debugging. - For region-specific behavior, read
req.geo(on Vercel). It is free and avoids a GeoIP service call.
Wrap-up
Middleware is the right place for cross-cutting, fast, body-free decisions: auth gates, bucketing, redirects, geo routing. It is the wrong place for heavy work or anything that needs Node APIs. Keep it small, keep matchers tight, and stay under your CPU budget.
Related articles
- Next.js Deploying Next.js on Vercel: A Practical Guide
A hands-on walkthrough for shipping a Next.js app to Vercel — connecting Git, configuring environment variables, understanding preview deployments, and avoiding the usual production gotchas.
- Next.js Next.js Edge Config Tutorial: Ultra-Low-Latency Reads
Use Vercel Edge Config to serve feature flags, redirects, and small config blobs from the edge with single-digit-millisecond reads — when to use it, how it differs from KV, and a working example.
- Next.js Next.js Middleware: Auth, Redirects, and Edge Logic
Learn how to use Next.js middleware for authentication gates, locale routing, A/B tests, and edge logic with matcher config and runtime caveats.
- Next.js Error Boundaries in the Next.js App Router
How error.tsx, global-error.tsx, and not-found.tsx work in the Next.js App Router — when each one fires, how segments isolate failures, and patterns for recovery and observability.