Skip to content
C Codeloom
Next.js

Next.js Middleware and the Edge Runtime

What Next.js middleware actually is, how it runs on the Edge, what you can and cannot do there, and the patterns that work in production.

·5 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • How Next.js middleware fits in the request lifecycle
  • What the Edge runtime supports and excludes
  • How to do auth, redirects, and AB tests in middleware
  • Performance traps to avoid
  • When to skip middleware entirely

Prerequisites

  • Familiarity with Next.js App Router

Middleware in Next.js is code that runs before a request reaches a route. It runs on the Edge runtime, which is a stripped-down V8 environment deployed close to users. Powerful, but constrained, and easy to misuse. This post is about what middleware is really for and how to keep it fast and safe.

What middleware actually is

A single middleware.ts file at the project root exports a function that receives the request and returns a Response (or nothing, meaning “continue”). It runs on every matched request, before pages or route handlers.

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

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

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

Use matcher to scope it. A middleware that runs on every request, including static assets, is one of the easiest ways to slow your whole site down.

The Edge runtime in one paragraph

Edge functions are not Node. There is no fs, no child_process, no native modules. The standard library is a subset of Web APIs: fetch, Request, Response, URL, crypto.subtle, TextEncoder. Cold starts are tiny (under 50 ms) and execution is metered by CPU time, often capped at 50 ms per request. That budget is your guardrail.

Mental model

Client --> CDN edge --> middleware (Edge runtime)
                          |
          +---------------+----------------+
          v                                v
     rewrite / redirect           continue to route
                                        |
                                        v
                          Edge route OR Node server route
Request path with middleware

Middleware can rewrite, redirect, set cookies and headers, or short-circuit with its own response. It cannot read the request body and then forward it; it is meant to be a lightweight gate.

Hands-on: feature flags and AB test bucketing

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(req: NextRequest) {
  const res = NextResponse.next();
  let bucket = req.cookies.get('ab')?.value;
  if (!bucket) {
    bucket = Math.random() < 0.5 ? 'A' : 'B';
    res.cookies.set('ab', bucket, { path: '/', maxAge: 60 * 60 * 24 * 30 });
  }
  res.headers.set('x-ab-bucket', bucket);
  return res;
}

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

The downstream React Server Component reads the cookie or header and serves the right variant. Bucketing in middleware avoids a layout shift from client-side AB.

Hands-on: auth gate with JWT verification

JWT verification works on the Edge using jose, which uses crypto.subtle. Do not use jsonwebtoken; it depends on Node crypto.

import { jwtVerify } from 'jose';
import { NextResponse } from 'next/server';

const secret = new TextEncoder().encode(process.env.JWT_SECRET!);

export async function middleware(req) {
  const token = req.cookies.get('session')?.value;
  if (!token) return NextResponse.redirect(new URL('/login', req.url));
  try {
    await jwtVerify(token, secret);
  } catch {
    return NextResponse.redirect(new URL('/login', req.url));
  }
}

export const config = { matcher: ['/app/:path*'] };

Verify; do not authorize. Fine-grained checks (“can user X read project Y?”) belong in route handlers where you have a database connection.

Common pitfalls

  • Matching / with no exclusions. Every static asset hits middleware and you pay edge invocations for nothing.
  • Importing Node-only packages. Build will fail or, worse, succeed and fail at runtime on Vercel.
  • Doing a database round-trip in middleware. Most DBs are not edge-friendly; latency stacks up across regions.
  • Reading the request body. Middleware does not have body access for performance reasons; do that work in a route handler.
  • Setting cookies on a NextResponse.next() and assuming the downstream route sees them on the same request. It sees them only on subsequent requests.
  • Long await chains. Each await fetch adds to your 50 ms budget. Parallelize with Promise.all or push the work elsewhere.

Practical tips

  • Treat middleware as an L7 router with auth. If logic gets complex, it belongs in a route.
  • Use matcher aggressively. Exclude _next, api, images, and anything that does not need the gate.
  • Cache verification results in a signed cookie so you do not re-decode JWTs every request.
  • Log timings. Date.now() at entry and exit, send the delta in a header for debugging.
  • For region-specific behavior, read req.geo (on Vercel). It is free and avoids a GeoIP service call.

Wrap-up

Middleware is the right place for cross-cutting, fast, body-free decisions: auth gates, bucketing, redirects, geo routing. It is the wrong place for heavy work or anything that needs Node APIs. Keep it small, keep matchers tight, and stay under your CPU budget.