Next.jsIntermediate

Next.js Middleware: What It's For and When to Avoid It

Middleware runs before every matching request — that makes it powerful for auth and redirects, and easy to misuse for things that belong in a page instead.

DevFieldGuideJune 24, 2026 (updated July 7, 2026)6 min read
Share:

Middleware sits between an incoming request and your route — it runs on every matching request, before any page or API route handler. That positioning is exactly what makes it useful, and exactly what makes it easy to overuse.

What it's genuinely good for

Redirects and rewrites based on request data, without a client-side flash of the wrong content:

ts
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
 
export function middleware(request: NextRequest) {
  const country = request.geo?.country ?? "US";
 
  if (country === "IN" && !request.nextUrl.pathname.startsWith("/in")) {
    return NextResponse.redirect(new URL(`/in${request.nextUrl.pathname}`, request.url));
  }
}

Gatekeeping authenticated routes before any page code runs, so unauthenticated users never see even a flash of protected content:

ts
export function middleware(request: NextRequest) {
  const token = request.cookies.get("session")?.value;
 
  if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
}

A/B testing and feature flags based on cookies, before the page renders anything.

Request arrivesBefore any page renders
Middleware runsEdge runtime, matcher-scoped
Redirect / rewrite / headersOr NextResponse.next()
Route handler / pageOnly reached if middleware allows it

Where it's commonly misused

Data fetching or heavy computation. Middleware runs on the Edge runtime, which has real constraints — no full Node.js API surface, tighter execution limits, and it runs on every single matching request, including ones that don't need it. Fetching from a database or calling a slow API in middleware adds latency to every request that matches your matcher, not just the ones that need the data.

Full authentication logic. Middleware is the right place to check "is there a valid-looking session token" and redirect if not — it's the wrong place to verify a JWT signature against a remote key, hit a database to check permissions, or do anything with meaningful latency. Do the lightweight check in middleware; do the actual authorization in the page or API route itself.

Anything that only affects one or two routes. If middleware's matcher config only needs to apply to a couple of pages, a simple check inside those specific page.tsx/route.ts files is more readable than a global file that conditionally does something for a narrow case — middleware's value comes from applying uniformly across many routes.

The matcher matters

ts
export const config = {
  matcher: ["/dashboard/:path*", "/settings/:path*"],
};

Without a matcher, middleware runs on every request — including static assets and API routes that don't need it. Scoping it explicitly avoids unnecessary overhead on requests that have nothing to do with what the middleware checks.

The rule of thumb

Middleware is for decisions that need to happen before rendering starts and apply broadly: redirect, rewrite, or gate access. If you're reaching for await inside middleware to fetch meaningful data, that's usually a sign the logic belongs one layer down, in the route itself.

Chaining multiple concerns in one middleware

Real applications often need more than one middleware concern — auth gating and A/B testing, for instance — and Next.js only supports a single middleware.ts file per project. The pattern is composing checks sequentially inside that one function, each returning early only when it needs to:

ts
export function middleware(request: NextRequest) {
  const authResponse = checkAuth(request);
  if (authResponse) return authResponse;
 
  const experimentResponse = assignExperimentBucket(request);
  if (experimentResponse) return experimentResponse;
 
  return NextResponse.next();
}
 
function checkAuth(request: NextRequest) {
  const token = request.cookies.get("session")?.value;
  if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
  return null;
}

Each helper function returns either a response (short-circuiting the rest) or null (meaning "no action needed, keep evaluating"). This keeps unrelated concerns readable as separate functions even though they all still execute within the one middleware entry point Next.js allows.

Setting response headers instead of redirecting

Not everything middleware does needs to change where a request goes — it can also read and forward request data as headers into the actual page, useful for passing something computed in middleware (a geo-detected locale, a resolved A/B test bucket) down to server components without recomputing it there:

ts
export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  response.headers.set("x-user-country", request.geo?.country ?? "US");
  return response;
}

The page or layout then reads x-user-country from the incoming request headers (via headers() in a Server Component) rather than re-deriving the same geo lookup a second time — middleware becomes a single point of computation shared across everything downstream of it.

Middleware and caching

Because middleware runs on every matching request, it interacts directly with how much of your app can be served from a cache. A middleware that reads a cookie and returns different content based on it (an A/B test bucket, a locale) means that route can no longer be served identically to every visitor from a shared CDN cache — the response genuinely varies per request. Where possible, encode that variation into the URL itself (a rewrite to /en/page vs /fr/page, rather than the same URL rendering differently based on a cookie) so the resulting pages remain cacheable per-variant rather than fully dynamic on every request.

Troubleshooting

Best practices
  • Middleware isn't running at all — confirm the file is named exactly middleware.ts (or .js) at the project root (or inside src/, matching wherever your other top-level app code lives), not nested inside app/.
  • Redirect loop — usually means the redirect target itself also matches the middleware's matcher, so the middleware redirects the redirect. Exclude the destination path from the matcher, or add a condition that skips the check once already on the target route.
  • Works locally, breaks on deploy — some hosting platforms run middleware on a genuine edge runtime with a smaller API surface than Node.js. Anything using a Node-only API (certain crypto or file-system calls) inside middleware can fail only in production for exactly this reason.
Advertisement

Frequently Asked Questions

Advertisement
DevFieldGuide
DevFieldGuide

Editorial Team

Practical tutorials and developer tools, written and maintained by the DevFieldGuide team.

Enjoyed this article?

Get the next one straight to your inbox, along with the best of what we publish each week.

Related Articles

More in Next.js

View all