Cloudflare Workers vs Node.js: When Edge Wins
Your Node.js API adds 100ms of latency. Not because your code is slow—because physics.
Cloudflare Workers run on 300+ edge locations worldwide. Your request hits the closest one first. No VPC routing, no cold start, no regional bottleneck.
But Workers are not a Node.js replacement. They’re a scalpel. This is how to know when to use one.
What Edge Runtime Means (And Doesn’t)
Cloudflare Workers run on V8 isolates—tiny JavaScript contexts with no process overhead:
// Cloudflare Worker - runs in ~1ms globallyexport default { async fetch(request) { return new Response("Hello from edge", { headers: { "Cache-Control": "max-age=3600" } }); }};| Metric | Node.js (us-east-1) | Cloudflare Worker (global edge) |
|---|---|---|
| P50 latency (client in Tokyo) | 140ms | 18ms |
| P95 latency (client in Sydney) | 180ms | 22ms |
| Time to first byte | ~50ms | ~2ms |
| Cold start | N/A | 0ms (always warm) |
That 160ms win is physics you can’t buy your way out of on a traditional server.
What Workers Can’t Do (And This Matters)
⚠️ No filesystem. Workers can’t read/write disk. Use KV (key-value cache) or Durable Objects (stateful storage).
// This fails in Workers:const data = fs.readFileSync("config.json");
// This works:const data = await env.KV.get("config");⚠️ 30-second timeout. A long-running job that takes 2 minutes? Doesn’t fit. Move to background jobs (Queues, or call your origin Node.js service).
⚠️ No raw TCP/UDP. WebSockets work. Raw socket connections don’t. If you’re building an IRC client, use Node.js.
⚠️ Limited CPU time. Workers get ~50ms of CPU before they timeout. Parsing 100MB of JSON? Use your origin. Parsing 10KB? Fine.
⚠️ No long-running connections. HTTP keep-alives timeout at 600 seconds. WebSocket can hold longer, but state isn’t persisted across requests.
Real Use Cases: Where Edge Wins
1. Auth Middleware (✔ Perfect for Workers)
// wrangler.toml routes all requests through this Worker first[env.production]routes = [ { pattern = "example.com/*", zone_name = "example.com" }]
// worker.jsexport default { async fetch(request, env) { const token = request.headers.get("Authorization");
// Check if token is in cache (KV) const cached = await env.KV.get(`token:${token}`); if (cached === "valid") { return fetch(request); // Forward to origin }
// Cache miss? Validate at origin const origin = await fetch(new Request("https://origin.internal/verify", { method: "POST", body: JSON.stringify({ token }) }));
const { valid } = await origin.json(); if (valid) { await env.KV.put(`token:${token}`, "valid", { expirationTtl: 3600 }); return fetch(request); // Forward to origin }
return new Response("Unauthorized", { status: 401 }); }};Result: Token validation at edge (18ms) vs origin (140ms). 7x faster, cached, global.
2. Image Transforms & CDN
// Resize images on-the-fly, cache globallyexport default { async fetch(request, env) { const url = new URL(request.url); const width = url.searchParams.get("width") || 300; const cacheKey = `image:${url.pathname}:${width}`;
// Check if transformed version is cached const cached = await env.KV.get(cacheKey, { type: "arrayBuffer" }); if (cached) { return new Response(cached, { headers: { "Content-Type": "image/webp", "Cache-Control": "max-age=86400" } }); }
// Fetch original from origin const image = await fetch(`https://origin.com${url.pathname}`); // In real code, use a library like Cloudflare's Image Resizing
return image; }};Result: First user waits for resize. Second user gets cached 18ms response from nearest edge.
3. A/B Routing
// Route 5% of traffic to canary, 95% to stableexport default { async fetch(request, env) { const variant = Math.random() < 0.05 ? "canary" : "stable"; const target = variant === "canary" ? "https://canary.internal.com" : "https://stable.example.com";
const response = await fetch(target + new URL(request.url).pathname); response.headers.set("X-Variant", variant); return response; }};Result: No Node.js logic needed. Edge handles routing. Your origin stays simple.
4. Geo-Personalization
// Serve different content based on countryexport default { async fetch(request, env) { const country = request.headers.get("cf-ipcountry") || "US"; const currency = { US: "USD", GB: "GBP", JP: "JPY" }[country] || "USD";
const response = await fetch(request); // Inject currency into response (or cache different versions) return new Response(response.body, response); }};Result: Same endpoint, different data globally. No geography-aware routing needed at origin.
The Wrangler Setup
name = "my-worker"type = "javascript"account_id = "your-account-id"workers_dev = true
[env.production]name = "my-worker-prod"routes = [ { pattern = "api.example.com/*", zone_name = "example.com" }]kv_namespaces = [ { binding = "KV", id = "abc123..." }]
[build]command = "npm run build"cwd = "./"Deploy:
wrangler publish --env productionHybrid: Workers + Origin Node.js
The real pattern is not Workers instead of Node.js. It’s Workers in front of Node.js:
User Request ↓Cloudflare Worker (edge, 18ms) ├─ Cache hit? → Return immediately ├─ Auth valid? → Forward to origin └─ Stale/miss → Fetch from origin, cache, return ↓Your Node.js API (origin, 140ms) └─ Database, logic, everythingWorker latency is hidden. Users see:
- Cache hit: 18ms (edge only)
- Cache miss: 140ms origin + 18ms edge caching = 158ms first, 18ms second
Decision Matrix
| Need | Workers | Node.js |
|---|---|---|
| Global latency-sensitive requests | ✔ | ⚠️ Regional lag |
| Cache + serve at edge | ✔ | ⚠️ CDN only |
| Auth/rate-limiting layer | ✔ | ⚠️ Origin overhead |
| Long-running jobs (>30s) | ⚠️ Use origin | ✔ |
| Database queries | ⚠️ Call origin | ✔ |
| Complex business logic | ⚠️ Call origin | ✔ |
| Stateful connections | ⚠️ Durable Objects | ✔ |
| Team familiarity | ⚠️ New runtime | ✔ |
Summary
Workers are not Node.js.js replacements. They’re force multipliers for latency-sensitive, cache-friendly, stateless logic.
Use Workers for: ✔ Auth, routing, rate-limiting ✔ CDN/cache layers ✔ Geo-personalization ✔ Request filtering
Use Node.js (or call it from Workers) for: ✔ Database mutations ✔ Complex algorithms ✔ Background jobs ✔ Stateful services
The hybrid approach wins: edge for latency, origin for logic. You get physics-defying speed without sacrificing the stability of your existing stack.
Don’t ask “Workers or Node.js?” Ask “Workers in front of Node.js?”