Skip to content

Next.js Middleware: The Five Patterns That Earn Their Keep

Middleware runs on every request. Use it for things that have to happen before the page renders. Stop using it for everything else.

John Cravey with AIFounder4 min readUpdated Jul 6, 2026

Next.js middleware runs at the edge before any route handler. It’s the right place for things that have to happen before the page renders — redirecting users, setting headers, rewriting URLs. It’s the wrong place for almost anything else, and the wrong-place uses are where we see middleware silently slowing down whole client sites.

Free estimate · 2 minutes

Slow pages lose rankings and leads. Price the fix.

Build the estimate to see what a fast rebuild would look like for your site and what we would ship in the first thirty days.

What middleware is and what it isn’t

Middleware is a single function in `middleware.ts` at the root of your project. It receives every matching request and returns either a Response (redirect/rewrite/pass-through) or modifies headers. It runs at the edge, which means it’s fast but limited — no Node-only APIs, no large dependencies, no database calls (no good ones, anyway).

Middleware is not the right place for data fetching, complex business logic, or anything that needs Node APIs. If you find yourself wanting `fs.readFile` in middleware, you’ve picked the wrong layer.

Pattern 1: geolocation-based redirects

Cloudflare and Vercel both expose the visitor’s country in the request headers. Use it to route US visitors to a US-specific page, EU visitors to a GDPR-compliant variant, and so on. We use this on one e-commerce client to redirect EU users to the EU storefront automatically.

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(req: NextRequest) {
  const country = req.geo?.country ?? "US";
  if (country === "GB" || country === "DE" || country === "FR") {
    return NextResponse.redirect(new URL("/eu", req.url));
  }
}

export const config = { matcher: ["/((?!api|_next|.*\\..*).*)"] };

Pattern 2: auth checks for protected routes

Check for an auth cookie. If present, let the request through. If absent on a protected route, redirect to login. Do not validate the JWT in middleware unless you have to — JWT validation needs the secret, and middleware secrets get rotated awkwardly. We do a simple presence check in middleware and a full validation in the server component or route handler.

export function middleware(req: NextRequest) {
  const token = req.cookies.get("session")?.value;
  if (!token && req.nextUrl.pathname.startsWith("/admin")) {
    const url = req.nextUrl.clone();
    url.pathname = "/login";
    url.searchParams.set("next", req.nextUrl.pathname);
    return NextResponse.redirect(url);
  }
}

Pattern 3: A/B test variant selection

Assign a variant cookie on first visit. Rewrite subsequent requests to the variant page based on the cookie. This keeps the test logic out of the React render tree and gives every visitor a stable variant for the duration of their session.

Pattern 4: bot detection and rate limiting

Use the request headers (user agent, IP, country) to short-circuit obvious bot traffic before it hits your origin. We pair this with Cloudflare’s bot management for the heavy lifting and use middleware only for the last-mile filter on routes Turnstile doesn’t cover (like the API routes for our analytics endpoint).

Pattern 5: locale and language routing

Read the `accept-language` header or a locale cookie, rewrite to the matching locale prefix. Cleaner than client-side detection — the URL reflects the locale immediately, no flash of wrong-language content, no SEO downside.

The matcher: scope your middleware tightly

Middleware runs on every matching request — including static assets if you’re not careful. The default matcher pattern (`"/((?!api|_next|.*\\..*).*)"`) excludes API routes, Next internals, and anything with a file extension (images, fonts, JS). Use a matcher tight enough that you’re not running edge code on every image request, but loose enough that you cover the routes you care about.

What not to do in middleware

  • Database calls. Middleware runs at the edge with no Node APIs; even an HTTP call to your DB adds 100–300ms of latency.
  • Heavy logic. The whole point of edge middleware is single-digit-ms response. If your middleware does more than 50 lines of computation, it belongs in a route handler.
  • JWT validation with crypto.subtle. Possible but slow and awkward. Do it in the route handler.
  • Setting hundreds of cookies. Headers have a size limit and middleware errors silently if you blow past it.

Debugging middleware

Middleware errors don’t always surface — a malformed Response returns a 500 with no detail by default. Wrap your middleware body in a try/catch and log errors to your observability platform. We log to GA4 as custom events for non-fatal middleware decisions (variant assignments, geo redirects) so we can see what’s happening at the edge in aggregate.

When NOT to use middleware at all

If your need is per-route logic that runs after data fetching, that’s the route handler, not middleware. If your need is to add headers to all responses, that’s the deploy platform’s configuration (Vercel’s `vercel.json`, Coolify’s reverse proxy), not middleware. If your need is rewriting URLs based on database state, that’s SSR, not middleware. The rule of thumb: middleware is for fast, request-level decisions that can be made from headers alone.

How this lands across FH client work

Most FH client sites use middleware for exactly one thing: a single redirect for legacy URLs. The rest of their auth, A/B, and geo logic lives in route handlers or in the deploy platform. The bias is toward keeping middleware boring and small. If you’re shipping 200 lines of middleware on a marketing site, book an audit — there’s almost certainly a faster way to get the same behavior without paying edge latency on every request.

Answers

Frequently asked questions

What is middleware for, and what is it not for?

It runs before a request is handled, so it suits redirecting, rewriting, and setting headers. It is not an application layer: it has a limited runtime, it runs on every matched request, and heavy work there taxes every page load rather than the one route that needed it.

What are the patterns worth using it for?

Five: geolocation-based redirects, auth checks on protected routes, A/B variant selection, bot detection and rate limiting, and locale routing. All five share a shape, which is a cheap decision made before rendering, and that is exactly what the layer is good at.

How should auth checks in middleware work?

As a cheap presence check that redirects, with real authorization enforced where the data is accessed. Middleware is a gate rather than a guarantee, and a route that relies on it alone is protected only for users who arrive the way you expected.

Why does the matcher matter so much?

Because unmatched paths skip middleware entirely, and an over-broad matcher runs it on every asset and image request. Scoping tightly is the difference between a negligible cost and a tax on every request the site serves.

What must not go in middleware?

Database queries, heavy computation, large dependencies, and anything needing Node APIs the runtime does not provide. Each of those either fails outright or slows every matched request, and both failure modes are discovered later than they should be.

How do I debug middleware?

Log at the boundaries and check whether the request matched at all, because the most common bug is a matcher that does not include the path you are testing. The second most common is a redirect loop from redirecting a path the matcher also matches.

How do redirect loops happen?

When middleware redirects a request to a path its own matcher covers, so the redirected request is redirected again. The rule that prevents it is that a gate's redirect target must sit outside the gate's own matcher, and it is worth asserting in a test rather than remembering.

Can middleware do A/B testing?

Yes, and it is one of the better uses: assign a variant, set a cookie, rewrite to the variant path, all before render, with no client-side flash. The constraint is keeping the assignment cheap and stable, because it runs on every matched request.

When should I not use middleware at all?

When the same logic fits in the route itself, which is most of the time. A check that only one page needs belongs on that page. Middleware earns its place when the decision must happen before routing or must apply across many routes.

Does middleware run on static assets?

It does if the matcher lets it, which is a common and expensive mistake. Excluding static paths and image requests is one of the first things to check when a site feels uniformly slower after middleware was added.

How does middleware interact with caching?

A rewrite or a varying response can defeat caching for matched routes, which is easy to do accidentally when personalizing. If a previously static route starts behaving dynamically after middleware lands, that interaction is the first thing to check.

What is the cost of a badly scoped middleware?

A latency penalty on every request the matcher touches, paid on every page load by every user, including for pages that gained nothing from it. It is one of the few changes that can slow an entire site with code that looks small and correct.

Question we did not answer? Ask us directly and we will answer it here.

John Cravey, Founder
Written by
John Cravey
Founder

Founder of Frontend Horizon. Writes most of the long-form work on the FH blog.

Newer post
UTM Strategy for SMBs: The Tagging Conventions That Survive a Year
Older post
Google Search Console: AI Overviews Data and What to Do About the Clicks You’re Losing
Keep reading

More from the blog

Next.js·10 min

Speed Is a Ranking Factor: The Next.js Performance Checklist by Business Size

Google measures your speed with real users and ranks you on it. Next.js hands you the tools to win. Most sites leave them switched off.

Next.js·6 min

Next.js 16.1 in Production: The Migration Playbook We Run on Every FH Site

Next 16.1 is the lean target. Here’s the exact migration we run, what breaks, and what to delete after.

Next.js·4 min

Server Components vs Client Components: The Mental Model That Stops You Reaching for ‘use client’

Most teams add ‘use client’ because they’re scared. The bundle pays for it.