Skip to content

Fix: Vercel Edge Function Not Working — Runtime APIs, Bundle Size, DB Drivers, and Middleware

FixDevs ·

Quick Answer

How to fix Vercel Edge Function errors — Node.js APIs not available in Edge Runtime, 1MB bundle size limit, Postgres/MySQL drivers incompatible, streaming responses, geo headers, and Middleware vs Edge API Route.

The Error

You move a function to the Edge runtime and it fails with API errors:

[Error] The Edge Runtime does not support Node.js 'fs' module.
Learn more: https://nextjs.org/docs/messages/node-module-in-edge-runtime

Or the build fails with size limits:

Error: The Edge Function "api/hello" size is 2.3 MB and your plan size limit is 1 MB.

Or your Postgres client throws at runtime:

ReferenceError: tls is not defined

Or request.geo is undefined locally:

export default function handler(request: NextRequest) {
  console.log(request.geo);  // undefined locally; works on Vercel.
}

Why This Happens

Vercel’s Edge Runtime is a V8 isolate environment based on Web Standards (Fetch API, Web Streams, Web Crypto). It’s not Node.js. Three core constraints:

  • No Node.js APIs. No fs, path, child_process, net. Most crypto is available via Web Crypto. Anything using TCP sockets (raw DB drivers) breaks.
  • Bundle size limits. Edge Functions have a strict bundle ceiling (1 MB on Hobby, 4 MB on Pro/Enterprise after compression). Big dependencies don’t fit.
  • Code runs at the edge. Cold start is faster than Lambda (~50ms vs 200-1000ms) but each region has its own instance. Stateful patterns that worked in single-region Lambda don’t fit.

For DB access from Edge, you need:

  • A driver that uses HTTP or WebSocket, not raw TCP (e.g. @neondatabase/serverless, @vercel/postgres, Cloudflare D1 client).
  • Or a database that exposes an HTTP API (PlanetScale, Turso, Supabase REST).

Fix 1: Declare the Edge Runtime

For Next.js App Router:

// app/api/hello/route.ts
export const runtime = "edge";

export async function GET(request: Request) {
  return Response.json({ hello: "world" });
}

For Next.js Pages Router:

// pages/api/hello.ts
export const config = {
  runtime: "edge",
};

export default function handler(request: Request) {
  return Response.json({ hello: "world" });
}

For Next.js Middleware:

// middleware.ts
import { NextResponse } from "next/server";

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith("/admin")) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
}

export const config = {
  matcher: "/admin/:path*",
};

Middleware always runs on the Edge Runtime — you can’t switch it to Node.

Pro Tip: Use runtime = "nodejs" (the default) for anything that needs fs, native crypto, or large dependencies. Reserve Edge for low-latency, geo-distributed, lightweight handlers.

Fix 2: Find Node API Replacements

Common substitutions:

Node APIEdge equivalent
fs.readFileBundle files as imports or use a remote fetch
crypto.createHashcrypto.subtle.digest (Web Crypto)
crypto.randomUUIDcrypto.randomUUID() (also Web standard)
crypto.randomBytescrypto.getRandomValues(new Uint8Array(N))
Buffer.from(str)new TextEncoder().encode(str) for bytes
Buffer.toString("base64")btoa(...) for ASCII, Buffer.from(...).toString("base64") via polyfill
process.envAvailable — but only env vars allowlisted for the Edge Runtime
setTimeout(fn, ms)Available (Web standard)
path.joinManual string concat or import from a small polyfill

For SHA-256 hashing:

async function sha256(text: string): Promise<string> {
  const data = new TextEncoder().encode(text);
  const hashBuffer = await crypto.subtle.digest("SHA-256", data);
  return [...new Uint8Array(hashBuffer)]
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
}

For HMAC signing:

async function hmacSign(secret: string, message: string): Promise<string> {
  const key = await crypto.subtle.importKey(
    "raw",
    new TextEncoder().encode(secret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign"],
  );
  const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message));
  return [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, "0")).join("");
}

Common Mistake: Importing a library that does require("fs") even when you don’t call that code path. The bundler may include it in the Edge bundle and the runtime errors at startup. Check your imports — packages that don’t ship with conditional ESM exports often include Node-only code.

Fix 3: Use Edge-Compatible DB Drivers

Drivers that work on Edge (HTTP/WebSocket-based):

// Neon (Postgres):
import { neon } from "@neondatabase/serverless";

export const runtime = "edge";

export async function GET() {
  const sql = neon(process.env.DATABASE_URL!);
  const rows = await sql`SELECT * FROM users LIMIT 10`;
  return Response.json(rows);
}
// PlanetScale (MySQL):
import { connect } from "@planetscale/database";

export const runtime = "edge";

export async function GET() {
  const conn = connect({ url: process.env.DATABASE_URL });
  const result = await conn.execute("SELECT * FROM users LIMIT 10");
  return Response.json(result.rows);
}
// Turso (libSQL/SQLite at the edge):
import { createClient } from "@libsql/client";

const client = createClient({
  url: process.env.DATABASE_URL!,
  authToken: process.env.DATABASE_AUTH_TOKEN!,
});

export const runtime = "edge";
// Vercel Postgres (wraps Neon):
import { sql } from "@vercel/postgres";

export const runtime = "edge";

export async function GET() {
  const { rows } = await sql`SELECT * FROM users LIMIT 10`;
  return Response.json(rows);
}

Drivers that don’t work on Edge:

  • pg (Postgres) — uses TCP/TLS sockets.
  • mysql2 — TCP-based.
  • mongodb (Node driver) — TCP-based.
  • ioredis — TCP-based.
  • ORMs that wrap them (prisma without @prisma/adapter-neon, typeorm, sequelize).

For Prisma:

import { PrismaClient } from "@prisma/client";
import { PrismaNeon } from "@prisma/adapter-neon";
import { Pool } from "@neondatabase/serverless";

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const adapter = new PrismaNeon(pool);
const prisma = new PrismaClient({ adapter });

export const runtime = "edge";

Prisma 5.10+ has Edge-compatible adapters for Neon, PlanetScale, D1, and Turso.

Fix 4: Keep Bundles Small

The 1 MB / 4 MB limit is on compressed bundle size. Strategies:

Avoid large dependencies. Some Node-style libs include Node-only fallbacks that bundlers can’t tree-shake. Check next build output for the worst offenders.

Use lighter alternatives:

// Heavy: zod (~13 KB)
import { z } from "zod";

// Lighter: valibot (~2-3 KB)
import * as v from "valibot";

// Heavy: lodash
import _ from "lodash";  // Includes all of lodash

// Lighter: native or scoped
const groupBy = <T, K extends string>(arr: T[], key: (item: T) => K) =>
  arr.reduce((acc, item) => {
    const k = key(item);
    (acc[k] ||= []).push(item);
    return acc;
  }, {} as Record<K, T[]>);

Dynamic import for rarely-used branches:

if (request.headers.get("x-debug")) {
  const { debug } = await import("./debug-helper");
  return debug(request);
}

The debug helper isn’t in the main bundle.

Check the Vercel build output:

Route (app)                                Size     First Load JS
┌ ƒ /api/edge                              2.34 MB    ❌ Too big

If a route reports ƒ (Edge) and a size over the limit, the build fails on deploy.

Pro Tip: Use @next/bundle-analyzer to find what’s in the bundle:

const withBundleAnalyzer = require("@next/bundle-analyzer")({ enabled: true });
module.exports = withBundleAnalyzer(nextConfig);
ANALYZE=true npm run build

Fix 5: Streaming Responses

Edge functions support Server-Sent Events and streaming naturally:

export const runtime = "edge";

export async function GET() {
  const stream = new ReadableStream({
    async start(controller) {
      const encoder = new TextEncoder();
      for (let i = 0; i < 10; i++) {
        controller.enqueue(encoder.encode(`data: ${JSON.stringify({ count: i })}\n\n`));
        await new Promise((r) => setTimeout(r, 1000));
      }
      controller.close();
    },
  });

  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    },
  });
}

For AI streaming (LLM token-by-token):

import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";

export const runtime = "edge";

export async function POST(req: Request) {
  const { messages } = await req.json();
  const result = streamText({ model: openai("gpt-4o-mini"), messages });
  return result.toDataStreamResponse();
}

ai SDK works at the Edge by design — built around Web Streams.

Note: Vercel’s free tier caps Edge Function execution time at 25 seconds. For longer streams (long-running LLM responses), upgrade or move to Lambda.

Fix 6: Geolocation and Headers

Geo data comes via headers (x-vercel-ip-country, etc.) or request.geo (Next.js):

import type { NextRequest } from "next/server";

export const runtime = "edge";

export default function handler(request: NextRequest) {
  const country = request.geo?.country ?? "unknown";
  const region = request.geo?.region ?? "";
  const city = request.geo?.city ?? "";

  return Response.json({ country, region, city });
}

For raw headers (any framework):

const country = request.headers.get("x-vercel-ip-country");
const ip = request.headers.get("x-real-ip") ?? request.headers.get("x-forwarded-for");

Geo data is populated only on Vercel. Locally during next dev, request.geo is empty. Test with a deploy preview.

Common Mistake: Relying on geo for security checks (e.g. “block users from country X”). VPNs and CDN routing can make geo unreliable. Use it for UX (default language, currency), not for authorization.

Fix 7: Edge Config for Fast Reads

@vercel/edge-config provides a read-optimized key-value store:

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

export const runtime = "edge";

export async function GET() {
  const flag = await get<boolean>("show_banner");
  return Response.json({ flag });
}

Reads are ~1ms from any edge location. Useful for:

  • Feature flags
  • A/B test assignments
  • Site config (banner copy, allowed origins, etc.)
  • IP/country blocklists

Edge Config has size limits (~512 KB) and writes are slower than reads — design for “read often, write rarely.”

For larger data, use KV (Vercel KV or external Redis); for arbitrary blob data, Vercel Blob.

Fix 8: Middleware vs Edge API Route

Three Edge-runtime placements in Next.js:

  • Middleware (middleware.ts) — runs before every matched route. Cannot return a body for non-matched routes. Best for redirects, headers, auth gating.
  • Edge API Route (app/api/.../route.ts with runtime = "edge") — a full request handler at the edge. Returns any response.
  • Server Component with edge runtime — page renders at the edge. Set via export const runtime = "edge" on the page file.

Don’t use Middleware for heavy logic:

// middleware.ts — BAD: runs on every request
export async function middleware(request: NextRequest) {
  const user = await fetchUserFromDB();  // DB call per request — slow
  // ...
}

// Better: do it in Edge API route or page server component.

Middleware should be ~10ms or less. Heavy work goes in Edge API routes (still fast, but you’re not blocking page load).

Pro Tip: Use matcher in middleware config to scope what runs. Without it, middleware runs on every request including static assets:

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

The regex excludes static asset paths from middleware.

Still Not Working?

A few less-obvious failures:

  • process is not defined for some env vars. Only env vars marked accessible to Edge are populated. Set them via Vercel dashboard, not .env.local alone.
  • structuredClone not available. Older Edge runtimes lack it. Use JSON.parse(JSON.stringify(...)) or upgrade Node.js version target in next.config.js.
  • Date.now returns different values across regions. Each edge region has its own clock. For consistent timestamps in distributed counting, use a single source of truth (DB timestamp).
  • fetch is the global, not Node’s. Headers are set differently; node-fetch patterns may fail. Use standard fetch syntax.
  • Bundle includes unused imports. Tree-shaking may not work for some libs. Move imports inside functions to defer loading.
  • URL constructor behaves differently. Edge runtime URL spec is web-standard, not Node’s slightly looser version. Some quirks like trailing slashes differ.
  • Cookies via cookies() are read-only in middleware. Set them on the response:
const response = NextResponse.next();
response.cookies.set("session", token, { httpOnly: true });
return response;
  • React server-render fails at Edge. Some React features (Suspense streaming) work; others need adjustments. Test with pnpm build locally before deploying.

For related serverless and Next.js issues, see Vercel deployment failed, Next.js middleware not running, AWS Lambda cold start timeout, and Next.js app router fetch cache.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles