Back to Blog

Next.js Edge Runtime: Middleware Constraints, Edge Functions, and Geo-Routing

Master the Next.js Edge Runtime. Covers Edge vs Node.js runtime differences, middleware constraints, Vercel Edge Functions, geo-based routing, A/B testing at the edge, and when to use each runtime.

Viprasol Tech Team
April 6, 2027
12 min read

The Next.js Edge Runtime runs your code at CDN edge locations worldwide โ€” the same infrastructure that serves static assets. Requests are handled 5โ€“50ms from the user instead of routing all the way to your origin server. For middleware (auth checks, redirects, rewrites, geo-routing), this eliminates the latency penalty of server-side logic entirely.

The tradeoff: Edge Runtime is a constrained environment โ€” no Node.js APIs, no native modules, no filesystem access. This guide covers what you can and can't do at the edge, and the patterns that make edge runtime worth using.

Edge vs Node.js Runtime

FeatureEdge RuntimeNode.js Runtime
Cold start~0ms (always warm)100โ€“500ms (serverless)
Execution time limit30s (Vercel)No limit (self-hosted)
Memory limit128MB1.5GB
Node.js APIsโŒ Noneโœ… Full
Native modules (bcrypt, sharp)โŒโœ…
Prisma (connection pooling)โŒ Directโœ… With Accelerate/PgBouncer
crypto (Web Crypto API)โœ…โœ…
fetchโœ…โœ…
Response, Request, Headersโœ…โœ…
File system (fs)โŒโœ…
Geo headersโœ… Via Vercel/CloudflareโŒ
Price (Vercel)Edge invocations (cheaper)Function invocations (pricier)

Choosing Your Runtime

// Route segment config โ€” applies to page/route/layout
export const runtime = "edge";    // Edge Runtime
export const runtime = "nodejs";  // Node.js (default)

// Middleware always runs at the edge (no config needed)
// middleware.ts is always Edge Runtime

Use Edge Runtime for:

  • Middleware (auth, redirects, rewrites, A/B)
  • Geo-based routing and personalization
  • JWT verification (Web Crypto API โœ…)
  • Simple API responses that don't need DB

Use Node.js Runtime for:

  • Database queries (Prisma, pg)
  • File operations (PDF generation, image processing)
  • Packages with native bindings (bcrypt, sharp, canvas)
  • Long-running computations

๐ŸŒ Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different โ€” senior engineers only, direct Slack access, and a 5.0โ˜… Upwork record across 100+ projects.

  • React, Next.js, Node.js, TypeScript โ€” production-grade stack
  • Fixed-price contracts โ€” no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

Middleware: Auth at the Edge

// middleware.ts โ€” always Edge Runtime
import { NextRequest, NextResponse } from "next/server";
import { jwtVerify, createRemoteJWKSet } from "jose";

const PROTECTED_PATHS = ["/dashboard", "/settings", "/api/v"];
const PUBLIC_PATHS = ["/auth", "/api/auth", "/_next", "/favicon.ico"];

// JWKS is fetched once and cached across edge instances
const JWKS = createRemoteJWKSet(
  new URL(`${process.env.NEXT_PUBLIC_APP_URL}/api/auth/jwks`)
);

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

  // Skip public paths
  if (PUBLIC_PATHS.some((p) => pathname.startsWith(p))) {
    return NextResponse.next();
  }

  // Only protect specified paths
  if (!PROTECTED_PATHS.some((p) => pathname.startsWith(p))) {
    return NextResponse.next();
  }

  const token = req.cookies.get("access_token")?.value;

  if (!token) {
    const loginUrl = new URL("/auth/signin", req.url);
    loginUrl.searchParams.set("callbackUrl", pathname);
    return NextResponse.redirect(loginUrl);
  }

  try {
    const { payload } = await jwtVerify(token, JWKS, {
      issuer: process.env.NEXT_PUBLIC_APP_URL,
    });

    // Forward user info to route handlers via headers
    const res = NextResponse.next();
    res.headers.set("x-user-id", payload.sub ?? "");
    res.headers.set("x-user-plan", (payload["plan"] as string) ?? "free");
    res.headers.set("x-org-id", (payload["orgId"] as string) ?? "");
    return res;
  } catch {
    // Invalid/expired token โ€” redirect to login
    const loginUrl = new URL("/auth/signin", req.url);
    loginUrl.searchParams.set("callbackUrl", pathname);
    const res = NextResponse.redirect(loginUrl);
    res.cookies.delete("access_token");
    return res;
  }
}

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.(?:png|jpg|svg|webp)$).*)"],
};

Geo-Based Routing

// middleware.ts โ€” geo routing with Vercel geo headers
import { NextRequest, NextResponse } from "next/server";
import { geolocation } from "@vercel/edge";

const COUNTRY_LOCALE_MAP: Record<string, string> = {
  DE: "de", FR: "fr", ES: "es", IT: "it", PT: "pt",
  JP: "ja", KR: "ko", CN: "zh", BR: "pt-BR", MX: "es",
};

const EU_COUNTRIES = new Set([
  "AT","BE","BG","CY","CZ","DE","DK","EE","ES","FI",
  "FR","GR","HR","HU","IE","IT","LT","LU","LV","MT",
  "NL","PL","PT","RO","SE","SI","SK",
]);

export function middleware(req: NextRequest) {
  const { country, city, region } = geolocation(req);
  const { pathname } = req.nextUrl;

  // Geo-based locale redirect (if not already localized)
  if (pathname === "/" && country) {
    const locale = COUNTRY_LOCALE_MAP[country];
    if (locale) {
      const url = req.nextUrl.clone();
      url.pathname = `/${locale}`;
      return NextResponse.redirect(url, { status: 302 });
    }
  }

  // EU GDPR banner: set cookie for client-side consent UI
  const res = NextResponse.next();
  if (country && EU_COUNTRIES.has(country)) {
    res.cookies.set("geo_region", "eu", {
      maxAge: 86400,
      httpOnly: false,  // Client reads this to show consent banner
    });
  }

  // Pass geo data to server components via headers
  if (country) res.headers.set("x-geo-country", country);
  if (city) res.headers.set("x-geo-city", city);
  if (region) res.headers.set("x-geo-region", region);

  return res;
}

๐Ÿš€ Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4โ€“8 weeks, full platforms in 3โ€“5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

A/B Testing at the Edge

// middleware.ts โ€” bucket users into variants at the edge, before any render
import { NextRequest, NextResponse } from "next/server";

const EXPERIMENTS: Record<string, { variants: string[]; weight: number[] }> = {
  "pricing-page-layout": {
    variants: ["control", "compact", "comparison-table"],
    weight:   [0.5,       0.25,      0.25],
  },
  "onboarding-flow": {
    variants: ["original", "checklist"],
    weight:   [0.5,        0.5],
  },
};

function assignVariant(
  userId: string,
  experimentId: string,
  weights: number[]
): number {
  // Deterministic assignment: same user always gets same variant
  const hash = Array.from(`${userId}:${experimentId}`).reduce(
    (acc, char) => ((acc << 5) - acc + char.charCodeAt(0)) | 0,
    0
  );
  const normalized = Math.abs(hash) / 2147483647;

  let cumulative = 0;
  for (let i = 0; i < weights.length; i++) {
    cumulative += weights[i];
    if (normalized < cumulative) return i;
  }
  return weights.length - 1;
}

export function middleware(req: NextRequest) {
  const res = NextResponse.next();

  // Get or create stable user ID for assignment
  let userId = req.cookies.get("ab_user_id")?.value;
  if (!userId) {
    userId = crypto.randomUUID();
    res.cookies.set("ab_user_id", userId, {
      maxAge: 365 * 24 * 60 * 60,
      httpOnly: true,
      sameSite: "lax",
    });
  }

  // Assign all experiments
  for (const [expId, exp] of Object.entries(EXPERIMENTS)) {
    const cookieKey = `ab_${expId}`;
    let variant = req.cookies.get(cookieKey)?.value;

    if (!variant || !exp.variants.includes(variant)) {
      const idx = assignVariant(userId, expId, exp.weight);
      variant = exp.variants[idx];
      res.cookies.set(cookieKey, variant, {
        maxAge: 30 * 24 * 60 * 60,
        httpOnly: false, // Client reads for analytics
        sameSite: "lax",
      });
    }

    // Forward to server components
    res.headers.set(`x-ab-${expId}`, variant);
  }

  return res;
}
// app/pricing/page.tsx โ€” read variant in Server Component
import { headers } from "next/headers";
import { PricingControl } from "@/components/pricing/control";
import { PricingCompact } from "@/components/pricing/compact";
import { PricingComparison } from "@/components/pricing/comparison-table";

export default async function PricingPage() {
  const headersList = await headers();
  const variant = headersList.get("x-ab-pricing-page-layout") ?? "control";

  // Each variant renders different component โ€” no client JS needed
  if (variant === "compact") return <PricingCompact />;
  if (variant === "comparison-table") return <PricingComparison />;
  return <PricingControl />;
}

Edge API Routes

// app/api/geo/route.ts โ€” edge API route, runs at CDN edge
export const runtime = "edge";

import { geolocation } from "@vercel/edge";
import { NextRequest, NextResponse } from "next/server";

export async function GET(req: NextRequest) {
  const { country, city, region, latitude, longitude } = geolocation(req);

  // Infer currency from country
  const CURRENCY_MAP: Record<string, string> = {
    US: "USD", GB: "GBP", DE: "EUR", FR: "EUR", JP: "JPY",
    IN: "INR", AU: "AUD", CA: "CAD",
  };

  return NextResponse.json({
    country: country ?? "US",
    city,
    region,
    coordinates: latitude && longitude
      ? { lat: parseFloat(latitude), lng: parseFloat(longitude) }
      : null,
    currency: (country && CURRENCY_MAP[country]) ?? "USD",
  });
}

JWT Verification at the Edge

Web Crypto API (crypto.subtle) works at the edge โ€” use it for HMAC verification:

// lib/edge/verify-hmac.ts โ€” runs in middleware (edge)
export async function verifyHmacSignature(
  payload: string,
  signature: string,
  secret: string
): Promise<boolean> {
  const encoder = new TextEncoder();
  const key = await crypto.subtle.importKey(
    "raw",
    encoder.encode(secret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["verify"]
  );

  const sigBuffer = hexToBuffer(signature);
  return crypto.subtle.verify("HMAC", key, sigBuffer, encoder.encode(payload));
}

function hexToBuffer(hex: string): ArrayBuffer {
  const bytes = new Uint8Array(hex.length / 2);
  for (let i = 0; i < hex.length; i += 2) {
    bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
  }
  return bytes.buffer;
}

What Doesn't Work at the Edge

// โŒ These will fail at runtime in Edge Runtime:

// 1. Prisma (requires Node.js TCP connections)
import { prisma } from "@/lib/prisma"; // โŒ Use Prisma Accelerate for edge

// 2. bcrypt (native module)
import bcrypt from "bcryptjs"; // โŒ Use Web Crypto subtle for edge-compatible hashing

// 3. File system
import fs from "fs"; // โŒ No filesystem at edge

// 4. Child processes
import { exec } from "child_process"; // โŒ

// 5. Most npm packages that use Node.js APIs
// Check with: https://edge-runtime.vercel.app/features/available-apis

Prisma at the Edge (with Accelerate)

// lib/prisma-edge.ts โ€” for edge routes that need DB access
import { PrismaClient } from "@prisma/client/edge";
import { withAccelerate } from "@prisma/extension-accelerate";

export const prismaEdge = new PrismaClient({
  datasourceUrl: process.env.DATABASE_URL_ACCELERATE, // Prisma Accelerate proxy URL
}).$extends(withAccelerate());

// In edge route:
export const runtime = "edge";

export async function GET() {
  // Works at edge via Accelerate's HTTP-based proxy
  const count = await prismaEdge.user.count({
    cacheStrategy: { ttl: 60 }, // Cache result for 60s at edge
  });
  return Response.json({ count });
}

Cost and Timeline Estimates

ScopeTeamTimelineCost Range
Auth middleware at edge1 dev1โ€“2 days$300โ€“600
Geo routing + locale redirect1 dev1โ€“2 days$300โ€“600
A/B testing infrastructure at edge1 dev3โ€“5 days$800โ€“1,500
Full edge optimization (audit + migration)1โ€“2 devs1โ€“2 weeks$3,000โ€“6,000

Vercel pricing:

  • Edge Middleware: included in all plans, 1M requests/month free
  • Edge Functions: $0.65 per 1M invocations (vs $1.80 for serverless functions)

See Also


Working With Viprasol

Edge Runtime unlocks genuine performance wins for auth middleware, geo-routing, and A/B testing โ€” but the constraint set is real, and moving the wrong code to the edge breaks things silently. Our team audits which parts of your Next.js stack belong at the edge, migrates middleware correctly, and instruments edge functions for observability.

What we deliver:

  • JWT verification middleware with jose at the edge
  • Geo-routing with country โ†’ locale and currency mapping
  • A/B testing with deterministic SHA-based variant assignment
  • Edge API routes for low-latency, high-volume endpoints
  • Prisma Accelerate setup for edge DB access

Talk to our team about your Next.js edge architecture โ†’

Or explore our web development services.

Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms โ€” we build it all with Next.js and React.

Free consultation โ€ข No commitment โ€ข Response within 24 hours

Viprasol ยท Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse โ‰ฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ€” from architecture through deployment.