Fix: Vercel Edge Function Not Working — Runtime APIs, Bundle Size, DB Drivers, and Middleware
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-runtimeOr 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 definedOr 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. Mostcryptois 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 API | Edge equivalent |
|---|---|
fs.readFile | Bundle files as imports or use a remote fetch |
crypto.createHash | crypto.subtle.digest (Web Crypto) |
crypto.randomUUID | crypto.randomUUID() (also Web standard) |
crypto.randomBytes | crypto.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.env | Available — but only env vars allowlisted for the Edge Runtime |
setTimeout(fn, ms) | Available (Web standard) |
path.join | Manual 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 (
prismawithout@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 bigIf 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 buildFix 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.tswithruntime = "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 definedfor some env vars. Only env vars marked accessible to Edge are populated. Set them via Vercel dashboard, not.env.localalone.structuredClonenot available. Older Edge runtimes lack it. UseJSON.parse(JSON.stringify(...))or upgrade Node.js version target innext.config.js.Date.nowreturns 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).fetchis the global, not Node’s. Headers are set differently;node-fetchpatterns may fail. Use standardfetchsyntax.- Bundle includes unused imports. Tree-shaking may not work for some libs. Move imports inside functions to defer loading.
URLconstructor 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;Reactserver-render fails at Edge. Some React features (Suspense streaming) work; others need adjustments. Test withpnpm buildlocally 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Vercel Blob Not Working — put/get/del, handleUpload Browser Flow, Access Modes, and Multipart
How to fix Vercel Blob errors — BLOB_READ_WRITE_TOKEN missing, put vs handleUpload for browser, public vs private access, multipart upload for large files, expires/signed URLs, list/cursor pagination, and overwriting URLs.
Fix: AWS Lambda SnapStart Not Working — Version vs Alias, Restore Hooks, and Uniqueness Bugs
How to fix Lambda SnapStart errors — feature requires published version, $LATEST not supported, restore hook for stale connections, UUID collisions after snapshot, time-based state staleness, and pricing surprises.
Fix: AWS Step Functions Not Working — ASL Syntax, Map State, Error Handling, and IAM
How to fix AWS Step Functions errors — Amazon States Language syntax, Standard vs Express workflows, Distributed Map for large datasets, Retry/Catch error handling, Lambda invoke optimization, and IAM execution role permissions.
Fix: Cloudflare Durable Objects Not Working — ID Strategy, Storage API, WebSocket Hibernation, Alarms
How to fix Cloudflare Durable Objects errors — idFromName vs newUniqueId, Storage transactions, blockConcurrencyWhile, WebSocket Hibernation API, alarms, migrations, and class binding setup.