Edge runtimes look like Node but aren't. Here's what actually breaks — CPU caps, no filesystem, no TCP sockets — and how we route around it.
We moved an auth-heavy API route to Cloudflare Workers expecting a free latency win. Deploy went green. Then a chunk of requests started returning 1102 "Worker exceeded CPU time" under load. The code ran fine locally on Node. That gap — runs on Node, dies at the edge — is the whole story, and it catches people every release.
Edge runtimes are not Node. Workers, Vercel Edge, and Deno Deploy are V8 isolates. You get JavaScript, the standard web platform (fetch, Request, Response, URL, streams, WebCrypto), and almost nothing from Node's standard library. No fs. No net. No child_process. No process-level threads. The isolate boots in single-digit milliseconds because there's no Node process behind it, and that speed is exactly why the APIs you lean on aren't there.
CPU time, not wall-clock. This is the one that bit us. Workers meter CPU time — actual compute — separately from wall-clock. On the paid plan you get up to 30 seconds of CPU, but the default limit is 50ms, and the free plan sits near 10ms. Awaiting a slow subrequest costs wall-clock but almost no CPU, so I/O-bound code is fine. A tight loop hashing a password is pure CPU and blows the budget:
// Runs locally, times out at the edge under load
export default {
async fetch(req) {
const body = await req.text();
// bcrypt-style work factor = lots of CPU, zero I/O
const hash = await slowHash(body, { rounds: 12 });
return new Response(hash);
},
};
The fix wasn't a faster hash. It was recognizing that per-request key stretching doesn't belong in an isolate at all.
Memory, ~128MB. Per isolate. Load a 40MB model or buffer a large upload fully into memory and you'll OOM. Streaming is the escape hatch — pass the body through instead of collecting it.
Bundle size. Workers cap the compressed script around 1MB on the free plan, 10MB on paid. Vercel Edge Functions sit near 1–4MB depending on plan. That kills "just npm install it" for anything dragging in a Node polyfill tree. sharp, bcrypt, aws-sdk v2 — all too big or too Node-dependent to bundle.
Subrequests. Each Worker invocation caps outbound fetch calls (50 on free, 1000 on paid). A fan-out that hits twenty microservices per request quietly hits the wall.
You can raise the CPU ceiling and lower it to fail fast in wrangler.toml:
name = "auth-api"
main = "src/index.ts"
compatibility_date = "2024-09-23"
[limits]
cpu_ms = 50 # fail fast in staging; raise to 30000 only where the work is justified
Here's the sharp edge nobody warns you about: there are no long-lived TCP connections. Postgres, MySQL, Redis, Mongo — their native drivers open a raw socket and keep it warm through a pool. Isolates can't open sockets and don't persist between requests, so those drivers either fail to import or hang on connect.
// pg — opens a TCP socket. Doesn't work in a V8 isolate.
import { Client } from "pg";
const client = new Client({ connectionString: process.env.DATABASE_URL });
await client.connect(); // no socket API → dead on arrival
The pattern that works is an HTTP/fetch-based client talking to a proxy that holds the real pool: Neon's serverless driver, PlanetScale's, Supabase's PostgREST, Upstash for Redis. Same query, different transport.
// Neon over HTTP fetch — no socket, connection pooling lives upstream
import { neon } from "@neondatabase/serverless";
const sql = neon(process.env.DATABASE_URL);
const rows = await sql`select id from users where email = ${email}`;
Even here, watch the subrequest cap: every query is an HTTP call.
Heavy crypto (bcrypt, scrypt, large RSA keygen), image processing (sharp, ImageMagick bindings), PDF generation, anything importing fs or assuming __dirname, and fat SDKs written for Node — these don't belong in an isolate. This is the same tradeoff we walk through in our edge computing playbook: the edge is for cheap, I/O-shaped, latency-sensitive work, and the origin is for everything heavy.
Detection is mostly mechanical. Set your dev runtime to match production — wrangler dev or Vercel's edge target, not plain next dev — so a missing fs fails on your machine, not in prod. Then:
crypto for crypto.subtle (WebCrypto). SHA-256, HMAC, AES-GCM, ECDSA are all there and run native.fetch.WebCrypto covered our HMAC signing cleanly:
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(payload));
Treat the edge as a fast, dumb front door, not a place to port your server. Put routing, auth-token verification, redirects, header rewrites, cache logic, and HTTP-based DB reads there — work that's I/O-shaped and finishes in a few milliseconds of CPU. Keep anything that hashes, resizes, buffers big blobs, or wants a socket back at the origin. Run wrangler dev locally and pin a low cpu_ms in staging so the timeout shows up on your laptop instead of in a customer's 1102. Do that and the edge earns its latency win. Fight the isolate model and you'll spend the savings debugging CPU timeouts at 2am.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
Our best engineer quit citing on-call. We rebuilt the whole thing: saner rotations, runbooks that actually help at 3am, and escalation that doesn't punish asking for help.
We ripped every client secret out of our CI pipelines by pointing Azure federated credentials at GitHub's OIDC issuer. Here's the exact setup and the claims that trip people up.
Explore more articles in this category
The edge is fast because it's constrained. This is the decision map for what belongs at the edge, what belongs at origin, and how compute, data, caching, and auth fit together.
Static keys leak. The question isn't if but how fast you notice and how clean your response runbook is when the pager goes off.
Edge code runs in hundreds of PoPs, lives for milliseconds, and gives you no shell. Here's how we get logs, traces, and metrics out of it anyway.
Evergreen posts worth revisiting.