Next.js Middleware: Everything You Can Do Before a Request Reaches Your Page
Middleware runs at the edge before your page renders — making it the most powerful and least-understood feature in Next.js. Here's what you can build with it.
What Middleware Is
Next.js Middleware is a function that runs before a request is matched to a route. It runs on Vercel's Edge Network (or your CDN/server edge), which means it executes geographically close to the user — without cold starts.
The key constraint: Middleware runs in the Edge Runtime. No Node.js APIs, no native modules, no direct database access (unless via HTTP-based clients). This constraint is what makes it fast.
// middleware.ts (in root of project)
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
return NextResponse.next(); // Pass through — do nothing
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};
The matcher config controls which paths trigger your middleware. Keep it specific — running middleware on /_next/static files serves no purpose and adds latency.
Authentication at the Edge
The most common middleware use case: check for an auth session before serving protected pages.
With a JWT-based session (the session is in a cookie, verifiable without a database call):
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose';
const protectedPaths = ['/dashboard', '/settings', '/billing'];
export async function middleware(request: NextRequest) {
const isProtected = protectedPaths.some(path =>
request.nextUrl.pathname.startsWith(path)
);
if (!isProtected) return NextResponse.next();
const token = request.cookies.get('session')?.value;
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
try {
await jwtVerify(token, new TextEncoder().encode(process.env.JWT_SECRET));
return NextResponse.next();
} catch {
const response = NextResponse.redirect(new URL('/login', request.url));
response.cookies.delete('session');
return response;
}
}
With Clerk (verifies at edge without a database round-trip):
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isProtected = createRouteMatcher(['/dashboard(.*)', '/settings(.*)']);
export default clerkMiddleware((auth, req) => {
if (isProtected(req)) auth().protect();
});
export const config = { matcher: ['/((?!.*\\..*|_next).*)'] };
The advantage of edge auth: the redirect happens before your page even starts rendering — no flash of protected content, no wasted server-side rendering.
Geo-Based Routing
Vercel populates request.geo with the user's geographic data:
export function middleware(request: NextRequest) {
const country = request.geo?.country;
// Redirect to localized version
if (country === 'DE' && !request.nextUrl.pathname.startsWith('/de')) {
return NextResponse.redirect(new URL(`/de${request.nextUrl.pathname}`, request.url));
}
if (country === 'FR' && !request.nextUrl.pathname.startsWith('/fr')) {
return NextResponse.redirect(new URL(`/fr${request.nextUrl.pathname}`, request.url));
}
return NextResponse.next();
}
Or, forward the country as a header for the page to use without redirecting:
export function middleware(request: NextRequest) {
const response = NextResponse.next();
response.headers.set('x-user-country', request.geo?.country ?? 'US');
return response;
}
// In a Server Component, read the forwarded header
import { headers } from 'next/headers';
export default async function PricingPage() {
const country = (await headers()).get('x-user-country') ?? 'US';
const currency = country === 'GB' ? 'GBP' : 'USD';
return <PricingTable currency={currency} />;
}
A/B Testing at the Edge
Assign users to test variants at the edge, with zero performance cost:
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
// Check for existing variant assignment
let variant = request.cookies.get('ab-homepage')?.value;
if (!variant) {
// Assign variant (50/50 split)
variant = Math.random() < 0.5 ? 'control' : 'treatment';
}
// Rewrite to the variant URL without changing the browser URL
const url = request.nextUrl.clone();
if (request.nextUrl.pathname === '/') {
url.pathname = variant === 'treatment' ? '/landing-b' : '/landing-a';
}
const response = NextResponse.rewrite(url);
// Persist the variant in a cookie
if (!request.cookies.has('ab-homepage')) {
response.cookies.set('ab-homepage', variant, { maxAge: 60 * 60 * 24 * 30 });
}
return response;
}
Users see the same URL in their browser but get different page content. The variant cookie ensures they see the same variant on subsequent visits.
Rate Limiting at the Edge
Basic rate limiting without a database (using cookies for state):
export function middleware(request: NextRequest) {
if (!request.nextUrl.pathname.startsWith('/api/')) {
return NextResponse.next();
}
// For real rate limiting, use Upstash Redis — HTTP-based, works at edge
// This example shows the pattern; production needs distributed state
const rateLimit = request.cookies.get('rate-limit-count');
const count = parseInt(rateLimit?.value ?? '0', 10);
if (count > 100) {
return new NextResponse('Too Many Requests', {
status: 429,
headers: { 'Retry-After': '60' },
});
}
const response = NextResponse.next();
response.cookies.set('rate-limit-count', String(count + 1), { maxAge: 60 });
return response;
}
For production rate limiting, use Upstash Redis — it's an HTTP-native Redis that works in the Edge Runtime.
Security Headers
Add security headers to every response without touching individual pages:
const securityHeaders = {
'X-Frame-Options': 'SAMEORIGIN',
'X-Content-Type-Options': 'nosniff',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
};
export function middleware(request: NextRequest) {
const response = NextResponse.next();
Object.entries(securityHeaders).forEach(([key, value]) => {
response.headers.set(key, value);
});
return response;
}
Note: you can also set security headers in next.config.ts via the headers() function. The middleware approach is better when headers depend on request context; the config approach is better for global, unconditional headers.
What Not to Do in Middleware
- Don't call your database directly — use HTTP-based clients (Neon serverless, Upstash, PlanetScale HTTP) or verify sessions via JWT
- Don't run heavy computation — middleware must be fast; anything over 100ms defeats the purpose
- Don't match too broadly — every matched path runs the middleware; unnecessary matches add latency to your static assets
- Don't use Node.js APIs — they're not available in the Edge Runtime
Middleware is a sharp tool: powerful when used for its specific purpose (request interception, fast edge logic), problematic when overloaded with concerns that belong in Server Components or API routes.
Continue reading
Related articles
Rate Limiting in Next.js: Protecting Your API Routes
How to implement production-grade rate limiting in Next.js — with Middleware-level protection, per-user limits, and distributed rate limiting using Upstash Redis.
EngineeringNext.js Parallel Routes and Intercepting Routes: A Complete Guide
Parallel routes and intercepting routes are among the most powerful App Router primitives. This guide explains what they do, when to use them, and how to avoid the common pitfalls.
EngineeringVercel vs Netlify vs AWS Amplify for Next.js in 2026
A practical comparison of the three most common Next.js hosting platforms — Vercel, Netlify, and AWS Amplify — with real cost and capability trade-offs.
Stay informed
Get our monthly deep dives.
Engineering, design, and growth insights — once a month. No spam.
Browse all resources