Next.js Middleware Is a Doorman, Not a Firewall
middleware.ts is powerful, and most teams abuse it — redirecting every request, querying databases at the edge, and logging themselves into a self-inflicted outage. Here is what it is actually for, and the code to keep it fast.
- Published
- Oct 2, 2024
- Reading
- 9 min read
- Category
- Next.js
- Length
- ~604 words
- Sections
- 4
Oct 2, 2024 · 9 min read

Middleware is one of the most powerful primitives Next.js gives you and one of the easiest to misuse. It runs on every request, on the edge, before caching — which makes it perfect for a narrow set of jobs and dangerous for everything else. Most of the middleware I review is doing too much.
It runs on every request — internalise that
Middleware without a matcher runs for your pages, your API routes, your static files, your images, your favicon, your sitemap, and your webhooks.
1// this runs for /_next/static/*, /favicon.ico,2// /api/webhooks/stripe — everything3export function middleware(request: NextRequest) {4 if (!request.cookies.get("token")) {5 return NextResponse.redirect(new URL("/login", request.url));6 }7}1export const config = {2 matcher: [3 /*4 * Everything except:5 * - _next/static, _next/image (build output)6 * - favicon.ico, robots.txt, sitemap.xml7 * - anything with a file extension (images, fonts)8 */9 "/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|.*\\..*).*)",10 ],11};12 13// or, better, be explicit about the routes that need it:14export const config = {15 matcher: ["/dashboard/:path*", "/settings/:path*", "/admin/:path*"],16};The edge runtime is not Node
Middleware runs in a constrained edge runtime: no Node APIs, no native database drivers, tight CPU and time limits. Verifying a signed token is fine — that is cryptography, not I/O. Looking up a session in your database is not.
1import { jwtVerify } from "jose"; // works on the edge, no Node crypto2 3const secret = new TextEncoder().encode(process.env.SESSION_SECRET!);4 5export async function middleware(req: NextRequest) {6 const token = req.cookies.get("session")?.value;7 if (!token) return NextResponse.redirect(new URL("/login", req.url));8 9 try {10 await jwtVerify(token, secret); // stateless — no DB round trip11 return NextResponse.next();12 } catch {13 const res = NextResponse.redirect(new URL("/login", req.url));14 res.cookies.delete("session");15 return res;16 }17}If you need the full user record or a permission check, do it in the route (a layout, a Server Component, or the action) — where you have the Node runtime, connection pooling, and the request-level cache. Middleware decides *where a request goes*, not *who the user is in detail*.
Logging in middleware is a bill you did not budget for
console.log("REQUEST:", request.url);console.log("COOKIES:", request.cookies.getAll());console.log("HEADERS:", Object.fromEntries(request.headers));This runs on every single matched request. On a site with real traffic that is gigabytes of logs an hour and an observability bill that spikes overnight. If you need request logging, sample it.
1if (Math.random() < 0.001) { // 0.1% of requests2 fetch(process.env.LOG_SINK!, {3 method: "POST",4 body: JSON.stringify({ path: req.nextUrl.pathname, ua: req.headers.get("user-agent") }),5 }).catch(() => {}); // never block the request6}What it is genuinely good at
1export function middleware(req: NextRequest) {2 const res = NextResponse.next();3 4 // locale routing off the Accept-Language header5 if (req.nextUrl.pathname === "/") {6 const locale = pickLocale(req.headers.get("accept-language"));7 if (locale !== "en") {8 return NextResponse.redirect(new URL(`/${locale}`, req.url));9 }10 }11 12 // sticky A/B bucket via a cookie — cheap, synchronous13 let bucket = req.cookies.get("ab")?.value;14 if (!bucket) {15 bucket = Math.random() < 0.5 ? "a" : "b";16 res.cookies.set("ab", bucket, { maxAge: 60 * 60 * 24 * 30 });17 }18 res.headers.set("x-ab-bucket", bucket);19 return res;20}| Use case | Verdict |
|---|---|
Redirect based on a cookie or token, with a matcher | Good |
Locale routing — / to /en or /ar | Good |
| A/B test bucketing via a header or cookie | Good |
| Rewriting a legacy URL to its new path | Good |
| Adding a security header to every response | Good |
| Full session auth with a database lookup | Bad |
Blocking the whole site except /login | Bad |
| Per-user rate limiting | Bad — use a dedicated edge rate limiter |
Middleware is not your backend. It is the doorman. He can glance at your ID and point you to the right floor. He does not run a background check on your whole family before letting you into the lobby.
Found this useful? Pass it on.
Want a custom write-up for your team? Get in touch.
Building something like this?
If a post here maps to a problem on your roadmap, that's usually a good sign we should talk.