Post 01
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.

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.