EdgeCases Logo
Apr 2026
Next.js
Deep
7 min read

Next.js Middleware Performance: What Runs at the Edge vs What Doesn't

Next.js middleware runs in Edge Runtime with strict limitations. Understanding what triggers Node.js fallback prevents performance regressions and function failures.

nextjs
middleware
edge-runtime
performance
cold-starts

Next.js middleware runs in Edge Runtime for sub-10ms cold starts—until you use incompatible APIs. Then it silently falls back to Node.js runtime with 250ms+ cold starts. Here's what triggers the fallback and how to stay fast.

Edge Runtime vs Node.js: The Performance Gap

Edge Runtime is a stripped-down JavaScript environment optimized for speed:

// Edge Runtime (✅ Fast: 1-10ms cold starts)
export function middleware(request: NextRequest) {
  const country = request.geo?.country;
  const url = request.nextUrl.clone();

  if (country === 'CN') {
    url.pathname = '/cn' + url.pathname;
    return NextResponse.redirect(url);
  }

  return NextResponse.next();
}

But add incompatible APIs and you get Node.js runtime:

// Node.js fallback (❌ Slow: 250-1000ms cold starts)
import fs from 'fs'; // ← Triggers Node.js runtime

export function middleware(request: NextRequest) {
  // This now runs in full Node.js environment
  const config = fs.readFileSync('./config.json');

  return NextResponse.next();
}

Performance impact: Edge Runtime delivers 1-10ms cold starts globally. Node.js runtime has 250-1000ms cold starts and runs regionally, not at every edge location.

APIs That Break Edge Runtime

These imports force Node.js runtime:

// Node.js built-ins (all trigger fallback)
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
import { Buffer } from 'buffer';

// Third-party packages using Node.js APIs
import bcrypt from 'bcrypt';      // Native crypto
import sharp from 'sharp';        // Image processing
import puppeteer from 'puppeteer'; // Headless browser

// Dynamic imports still trigger fallback
const crypto = await import('crypto');

Hidden fallback triggers: Dependencies can import Node.js APIs without you knowing. Check your bundle for Node.js polyfills—they indicate Edge Runtime incompatibility.

Edge Runtime Alternatives

Replace Node.js APIs with Web Standard equivalents:

// ❌ Node.js crypto (triggers fallback)
import crypto from 'crypto';
const hash = crypto.createHash('sha256').update(data).digest('hex');

// ✅ Web Crypto API (Edge Runtime compatible)
const encoder = new TextEncoder();
const data = encoder.encode(inputString);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hash = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');

// ❌ Buffer (triggers fallback)
const buffer = Buffer.from(str, 'base64');

// ✅ ArrayBuffer (Edge Runtime compatible)
const binaryString = atob(str);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
  bytes[i] = binaryString.charCodeAt(i);
}

Database Connections: The Biggest Gotcha

Traditional database drivers trigger Node.js runtime:

// ❌ Triggers Node.js runtime
import { Pool } from 'pg';
import mysql from 'mysql2';
import { MongoClient } from 'mongodb';

export function middleware() {
  // Now running in slow Node.js runtime
}

Use HTTP-based database clients instead:

// ✅ Edge Runtime compatible
import { createClient } from '@vercel/postgres';
import { PlanetScale } from '@planetscale/database';
import { createClient } from '@supabase/supabase-js';

export async function middleware(request: NextRequest) {
  // Still fast Edge Runtime
  const db = createClient({ connectionString: process.env.DATABASE_URL });
  const user = await db.sql('SELECT * FROM users WHERE id = ' + userId);

  return NextResponse.next();
}

Why this matters: HTTP-based clients use fetch() under the hood, which is Edge Runtime compatible. Traditional drivers use TCP sockets, which require Node.js.

Detecting Runtime Fallback

Check your middleware runtime in Vercel Functions dashboard or add logging:

export function middleware(request: NextRequest) {
  // This tells you which runtime you're actually using
  console.log('Runtime:', typeof process !== 'undefined' ? 'Node.js' : 'Edge');

  // Edge Runtime has limited globals
  console.log('Edge features:', {
    hasGeo: !!request.geo,
    hasWebCrypto: !!globalThis.crypto?.subtle,
    hasBuffer: typeof Buffer !== 'undefined'  // false in Edge
  });

  return NextResponse.next();
}

Vercel's function logs show runtime type and cold start duration. Edge functions show ~1-10ms, Node.js shows 200ms+.

Performance Optimization Strategies

1. Keep middleware lean:

// ✅ Fast: Simple checks only
export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/api/admin')) {
    const token = request.cookies.get('admin-token');
    if (!token) return new Response('Unauthorized', { status: 401 });
  }
  return NextResponse.next();
}

2. Move heavy logic to API routes:

// ✅ Middleware stays fast
export function middleware(request: NextRequest) {
  const needsValidation = request.headers.get('x-needs-auth');

  if (needsValidation) {
    // Redirect to API route for complex auth logic
    const url = request.nextUrl.clone();
    url.pathname = '/api/validate-session';
    return NextResponse.redirect(url);
  }

  return NextResponse.next();
}

3. Use Edge Config for fast data access:

import { get } from '@vercel/edge-config';

export async function middleware(request: NextRequest) {
  // 0-5ms globally, Edge Runtime compatible
  const blockedIPs = await get('blocked-ips') as string[];
  const clientIP = request.ip;

  if (blockedIPs.includes(clientIP)) {
    return new Response('Blocked', { status: 403 });
  }

  return NextResponse.next();
}

Bundle analysis tip: Use @next/bundle-analyzer to check for Node.js polyfills in your middleware. Polyfills indicate Edge Runtime incompatibility.

When Node.js Runtime Is Necessary

Some operations require Node.js runtime:

  • File system operations—reading config files, writing logs
  • Native crypto operations—bcrypt password hashing, complex JWT signing
  • Legacy database drivers—MySQL, PostgreSQL connection pools
  • Complex image processing—Sharp, ImageMagick operations

In these cases, accept the 250ms+ cold start penalty or move logic to API routes that can handle Node.js runtime without affecting edge performance.

The key insight: Next.js middleware runtime is determined by your imports, not configuration. Keep imports Web Standard-compatible to maintain Edge Runtime performance benefits.

Advertisement

Related Insights

Explore related edge cases and patterns

TypeScript
Expert
TypeScript Mapped Type Modifiers: When Inference Breaks
7 min
Next.js
Deep
Next.js 'use cache': Explicit Caching with Automatic Keys
9 min
Next.js
Deep
Next.js Parallel and Intercepting Routes
8 min

Advertisement