Discuss your task

Next.js Middleware: Geolocation and Auth on the Edge

Next.js Middleware is code that runs on the edge before a request reaches the cache or the app’s routing: it can redirect a user by country, check an auth cookie, and rewrite headers without waiting for the page to render. A single middleware.ts file at the project root intercepts matched requests before any React component runs. A correctly scoped matcher limits it to specific paths, so it adds no overhead even under heavy traffic.

In web development projects, we consistently see the same mistake: teams implement geolocation or auth checks on the client - via useEffect and a browser-side redirect after the page has already rendered. The user sees a landing page in the wrong language, or a protected section they shouldn’t have access to, for a fraction of a second before the redirect fires. Next.js Middleware solves both problems at the request level, before the first paint. Below: how it works, how behavior differs between Vercel and alternative hosts, and what actually affects performance.

For SaaS products with an international audience and B2B landing pages with gated sections, this problem shows up immediately. A product team displays a price in USD to a visitor from Germany, who expects EUR, and loses part of the conversion to that mismatch. Or an admin panel flashes for a split second to an unauthenticated visitor before client-side code manages to redirect them. Both are the result of a decision made too late - already in the browser.

Middleware: a function that runs on the edge for every matched request, before it reaches the cache, a static file, or the app router.

What runs before rendering: where Middleware sits in the request lifecycle

Middleware intercepts a request before static generation, the ISR cache, or route matching kick in. The function is declared in a single middleware.ts (or .js) file at the project root or in src/, and Next.js calls it for every request that matches the matcher. A detailed description of the lifecycle is in the official Next.js Middleware documentation.

Historically, Middleware ran only in the Edge Runtime - a lightweight environment without access to most Node.js modules: no direct TCP connections to a database, and a limited set of APIs. Starting with Next.js 15.2, experimental support for Node.js Middleware was added, which expands the available APIs, but the status remains experimental, and on most hosts other than Vercel support is still incomplete.

Inside Middleware you have access to NextRequest, and you return a NextResponse - a redirect, a rewrite, a response with modified headers, or simply NextResponse.next() to pass the request through unchanged. Middleware is a separate layer from the rendering architecture (Server Components and approaches like Astro Islands or React Server Components): it runs before it, not instead of it.

Example: a minimal middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  return NextResponse.next();
}

export const config = {
  matcher: '/((?!_next/static|_next/image|favicon.ico).*)',
};

Geolocation: personalization and country-based redirects

Geolocation in Middleware determines a user’s country, region, and city from their IP address at the edge, before the request reaches the page. On Vercel this is done through the geolocation() helper from @vercel/functions, or directly via the x-vercel-ip-country, x-vercel-ip-country-region, and x-vercel-ip-city headers that Vercel attaches to every request.

An important detail: in earlier versions of Next.js, geolocation was available through request.geo directly on the NextRequest object. In current versions that field has been removed from the types, and the officially recommended path is the geolocation() helper or reading the headers directly. If you see request.geo in an older guide, that’s a deprecated API.

A key caveat about hosting: the x-vercel-ip-* headers only appear on Vercel’s infrastructure. When deploying to Cloudflare, a self-hosted Node server, or another edge provider, geolocation needs a different source - for example, the CF-IPCountry header on Cloudflare, or a third-party IP geolocation service if the host doesn’t provide this data at all. Code that only reads x-vercel-ip-country silently stops working when you move off Vercel to another platform, and that’s worth checking before a hosting migration, not after.

Example: country-based redirect in middleware.ts
import { NextResponse } from 'next/server';
import { geolocation } from '@vercel/functions';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const { country } = geolocation(request);
  const url = request.nextUrl;

  if (country === 'DE' && !url.pathname.startsWith('/de')) {
    url.pathname = `/de${url.pathname}`;
    return NextResponse.redirect(url);
  }

  return NextResponse.next();
}

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

For multilingual sites, a geolocation redirect should be paired with correct hreflang markup, otherwise Google indexes a different language version than the one Middleware redirects to.

Auth: protecting routes without a flash of protected content

Flash of protected content is when an unauthenticated user briefly sees a protected page or its markup before client-side code manages to redirect them. Middleware eliminates this: the session cookie or JWT check happens on the edge, before the server even starts rendering the protected page.

The logic is simple: Middleware reads a cookie (for example, session or token), optionally verifies the JWT signature, and if the user isn’t authenticated, redirects to /login before rendering. Full JWT signature verification in the Edge Runtime should use a library compatible with the Web Crypto API (jose, not jsonwebtoken, which depends on Node.js modules).

Example: checking the auth cookie in middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const PROTECTED_PATHS = ['/dashboard', '/settings', '/admin'];

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const isProtected = PROTECTED_PATHS.some((path) => pathname.startsWith(path));

  if (!isProtected) {
    return NextResponse.next();
  }

  const token = request.cookies.get('session')?.value;

  if (!token) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('from', pathname);
    return NextResponse.redirect(loginUrl);
  }

  return NextResponse.next();
}

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

Middleware only checks whether a token exists and is superficially valid - this is the first line of defense. Full authorization checks (role, access to a specific resource) are usually left to the API route or server component, since those have a full runtime environment and can safely query the database.

Combining geolocation and auth in one middleware.ts

A project has one Middleware, so geolocation and auth logic have to be combined into a single function rather than written as separate files. The order of checks matters: run fast checks first (path match, cookie presence), then more expensive ones (JWT parsing).

Example: geolocation + auth in one middleware.ts
import { NextResponse } from 'next/server';
import { geolocation } from '@vercel/functions';
import type { NextRequest } from 'next/server';

const PROTECTED_PATHS = ['/dashboard', '/settings'];

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

  const isProtected = PROTECTED_PATHS.some((path) => pathname.startsWith(path));
  if (isProtected) {
    const token = request.cookies.get('session')?.value;
    if (!token) {
      const loginUrl = new URL('/login', request.url);
      return NextResponse.redirect(loginUrl);
    }
    return NextResponse.next();
  }

  const { country } = geolocation(request);
  if (country === 'GB' && !pathname.startsWith('/uk')) {
    const url = request.nextUrl.clone();
    url.pathname = `/uk${pathname}`;
    return NextResponse.redirect(url);
  }

  return NextResponse.next();
}

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

If the logic grows past 2-3 scenarios, it’s worth splitting the checks into separate functions within the same file: Next.js allows only one Middleware per app, but doesn’t restrict the internal code structure.

Performance: the matcher and what not to do in Middleware

Middleware runs on every request that matches the matcher, including requests for static assets if the matcher is set too broadly. A narrow matcher is the main performance lever: it determines which paths the function even runs for.

A typical mistake is the default matcher '/:path*', which runs Middleware on literally every request, including images and static files from _next/static. The correct pattern is to explicitly exclude service paths using a negative lookahead in the regular expression, as in the examples above.

The second rule is to avoid anything in Middleware that blocks the response for more than a few milliseconds. Requests to external APIs, database calls, heavy computation - all of this increases TTFB for every request that passes through the matcher, not just for one endpoint. Middleware is well suited to reading cookies, headers, and URL parameters, and poorly suited to any network operation with unpredictable latency.

If you need a check that requires calling an external service (for example, a database call to validate a token), the standard pattern is to cache the result in the JWT itself (claims) at issuance time, rather than querying it again in Middleware on every request.

Hosting determines how Middleware behaves

Middleware in Next.js is an abstraction over a specific edge infrastructure, and that has a real effect on where and how you should deploy it. On Vercel, Middleware runs in their own Edge Runtime with the full set of geolocation headers out of the box.

On Cloudflare Pages the situation is more complicated: Cloudflare now recommends deploying Next.js through OpenNext on Cloudflare Workers rather than through Cloudflare Pages directly, and some capabilities - in particular the Node.js Middleware introduced in Next.js 15.2 - aren’t yet supported by the OpenNext adapter for Cloudflare. Vercel’s edge runtime doesn’t match Cloudflare’s Workers runtime 100% on API surface, so Middleware written and tested on Vercel may need adjustments when you move it.

Before you build geolocation or auth into Middleware as part of your architecture, it’s worth comparing hosting options for a Next.js project up front: switching platforms later means rewriting this logic, not just redeploying it.

Real case: what changes in practice

In a typical project migrating a landing page from client-side auth checks to Middleware, we see two measurable effects. First, the visible flash of protected content disappears entirely - previously it was noticeable on slow connections for 200-400 ms between the first paint and the client-side redirect firing. Second, for geolocation redirects, TTFB to the final, already-localized page is usually lower than with a client-side redirect via useEffect, because the redirect happens before the HTML is sent to the browser, not after React hydration.

A separate effect from a narrow matcher: a 60-80% drop in Middleware edge invocations in typical projects where it was previously, mistakenly, also running on static assets - images, fonts, files from _next/static.

Who this is for

Middleware with geolocation and auth makes sense for SaaS products with an international audience that need regional personalization - pricing, language, legal notices - and for B2B sites with gated sections: client portals, dashboards, paid content. The effect is especially noticeable for teams without a dedicated backend developer, who need to solve both problems using Next.js itself, without standing up a separate auth service or reverse proxy.

Frequently asked questions

How is Middleware different from an auth check in an API route?

Middleware runs earlier - at the request level, before the page renders and before the route handler is called. It’s suited to a quick check of whether a user is authenticated at all, and to redirecting. Full authorization for a specific resource - database lookups, roles, permissions - is better handled in the API route or server component, where a full runtime environment is available.

Does request.geo work on any host?

No. Geolocation headers like x-vercel-ip-country only appear when deploying to Vercel. On other platforms you need a different data source - for example, the CF-IPCountry header on Cloudflare, or an external IP geolocation service. Code that depends only on Vercel’s headers stops working without an explicit error when you switch hosts, and that needs to be checked on every migration.

Does Middleware slow down every request on the site?

It does, if the matcher is set broadly and captures static assets, images, or service paths. With a narrow matcher limited to specific routes, and no network requests inside the function, the added latency is usually a few milliseconds at most - Middleware runs on the edge, geographically close to the user.

Can you query a database directly in Middleware?

Technically - only if you’re using the experimental Node.js Middleware (from Next.js 15.2) and your database driver’s provider is compatible with that environment. In the classic Edge Runtime, direct TCP connections to a database aren’t available. The standard pattern is to not query the database in Middleware at all, and instead verify the signature of a JWT that was issued with the necessary claims ahead of time.

What happens if you forget to set a matcher?

Without an explicit matcher, Middleware runs on every path by default, including static assets from _next/static and _next/image. This doesn’t break functionality, but it adds unnecessary edge invocations for every static asset request - for a high-traffic site, that’s a noticeable and easily avoidable performance cost.

Bottom line

  • Middleware runs on the edge before rendering - it’s the only place where a geolocation redirect or auth check happens before the browser receives any HTML.
  • Geolocation depends on the host: x-vercel-ip-country only works on Vercel; other platforms need their own data source.
  • Flash of protected content is eliminated by moving the cookie or JWT check into Middleware instead of a client-side useEffect.
  • A narrow matcher and no network operations inside the function are the main performance levers.

If you’re designing a Next.js site architecture and need to combine geolocation, auth, and hosting without reworking the logic after launch, describe your project to the Exceltic.dev team. We’ll review the architecture and estimate the scope of work.

More articles

All →