The most surprising thing about Vercel’s Edge Middleware is that it doesn’t run on your origin server at all; it executes before your request even hits your application, directly on Vercel’s global CDN.

Imagine a user in Tokyo requests your site. Instead of their request traveling all the way to your backend in, say, New York, and then back, Vercel’s Edge Middleware intercepts it at a Point of Presence (POP) geographically close to them. This is where your middleware code runs.

Here’s a simplified middleware.ts file:

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

export function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname;

  // Redirect requests from /old-path to /new-path
  if (pathname === '/old-path') {
    const url = request.nextUrl.clone();
    url.pathname = '/new-path';
    return NextResponse.redirect(url);
  }

  // Add a custom header to all responses
  const response = NextResponse.next();
  response.headers.set('X-Edge-Processed', 'true');

  return response;
}

export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
};

When a request comes in for /old-path, the middleware executes at the edge. It checks request.nextUrl.pathname. Seeing it matches /old-path, it creates a new URL pointing to /new-path and returns NextResponse.redirect. The user’s browser then makes a new request to /new-path, which is handled by the edge middleware again. This time, the if condition is false, so it proceeds to NextResponse.next(), adds the X-Edge-Processed: true header, and then the request is forwarded to your Next.js application (or static file).

The primary problem Edge Middleware solves is performance. By running code closer to the user, you drastically reduce latency for tasks like:

  • Authentication and Authorization: Checking JWTs or session cookies before a request even hits your backend.
  • Internationalization (i18n): Redirecting users to the correct language subdomain or path based on their IP or Accept-Language header.
  • A/B Testing: Rewriting URLs or adding headers to route users to different experiment variants.
  • Rewrites and Redirects: Dynamically changing request paths or responding with redirects without a full server roundtrip.

Internally, Vercel leverages Web Standard APIs (like Request, Response, URL) and compiles your middleware to a WebAssembly runtime (like wasmtime) that can be executed efficiently within their global network of edge servers. This is why you’ll see references to next/server and NextResponse, mirroring the Fetch API. The matcher in middleware.ts is crucial; it’s a glob pattern that tells Vercel which requests should have your middleware applied. If a request path doesn’t match, the middleware is skipped entirely, and the request goes straight to your origin.

The config.matcher is a powerful, yet often misunderstood, part of Edge Middleware. Most developers understand it as a way to include paths, but its primary function is to exclude paths from running the middleware. The pattern '/((?!api|_next/static|_next/image|favicon.ico).*)' is the default for Next.js projects. It means "match any path .* that does not start with api, _next/static, _next/image, or favicon.ico." This is critical because you generally don’t want your middleware running on static assets or API routes, as it adds unnecessary overhead. If you wanted to apply middleware to API routes, you’d modify this matcher. For instance, to include API routes, you might change it to '/((?!_next/static|_next/image|favicon.ico).*)'.

One common pitfall is forgetting that the middleware runs on every matched request before it reaches your Next.js pages or API routes. This means that if your middleware performs a redirect, the user’s browser will make a new request, and that new request will also be evaluated by the middleware. This can lead to infinite redirect loops if not handled carefully. For example, if you have a middleware that redirects /users to /profile and your pages/profile.js also has a redirect back to /users, you’re in trouble. Always ensure your redirect targets don’t accidentally trigger the same redirect logic.

The next step after mastering basic rewrites and redirects is understanding how to leverage the request.geo object to personalize responses based on the user’s geographical location.

Want structured learning?

Take the full Vercel course →