Skip to content
Unverified — AI-generated content. Help verify this page

Edge Runtime Constraints ​

Why Edge Runtimes Are Constrained ​

Edge runtimes sacrifice Node.js compatibility for extreme performance characteristics. V8 isolates start in under 5ms (vs 100-1000ms for containers), use ~128KB of memory per isolate (vs 50-200MB per container), and can run thousands of concurrent functions on a single machine. These properties are only possible because edge runtimes strip away everything that is heavy or dangerous:

  • No filesystem: The edge has no persistent disk. Eliminating fs removes an entire class of security vulnerabilities and operational complexity.
  • No native addons: C++ bindings require compilation per platform and break V8 isolate security guarantees.
  • Limited CPU time: Each request gets 10-50ms of CPU time (wall-clock time can be longer for I/O waits). This prevents any single request from monopolizing shared resources.
  • Limited memory: 128MB per isolate is typical. This is adequate for request processing but insufficient for large data processing.

The Web Standards Foundation ​

Edge runtimes implement the WinterCG (Web Interoperable Runtimes Community Group) specification, which standardizes a common API surface based on Web Platform APIs. If your code runs in a browser's Service Worker, it should run at the edge with minimal changes.

First Principles ​

What You Have ​

Edge runtimes provide a subset of Web Platform APIs:

*Some runtimes provide polyfills for path and other utility modules.

Resource Limits by Platform ​

LimitCloudflare WorkersDeno DeployVercel EdgeLambda@Edge
CPU time per request10-50ms (free) / 30s (paid)50ms (free) / 10s (paid)30s5s (viewer) / 30s (origin)
Wall-clock time30sUnlimited30s30s
Memory128MB512MB128MB128MB (viewer) / 1GB (origin)
Script size10MB (free) / 10MB+ (paid)No limit4MB50MB
Subrequest count50 (free) / 1000 (paid)UnlimitedUnlimitedUnlimited
Request body size100MBUnlimited4MB1MB (viewer) / 50MB (origin)
Environment variables64UnlimitedUnlimitedUnlimited

Core Mechanics ​

Working Without the Filesystem ​

Instead of fs, edge runtimes provide alternative storage:

typescript
// Node.js (traditional)
import fs from 'node:fs';
const config = JSON.parse(fs.readFileSync('./config.json', 'utf-8'));
const template = fs.readFileSync('./template.html', 'utf-8');

// Edge: Embed at build time
// config.json and template.html are bundled into the script
import config from './config.json';
const template = `<!DOCTYPE html><html>...</html>`;

// Edge: Fetch from KV store
const config = await env.CONFIG_KV.get('app-config', 'json');

// Edge: Fetch from URL
const config = await fetch('https://config.example.com/app.json')
  .then(r => r.json());

// Edge: Use R2 (object storage) for large files
const file = await env.R2_BUCKET.get('uploads/image.png');
if (file) {
  return new Response(file.body, {
    headers: { 'Content-Type': file.httpMetadata?.contentType || 'image/png' },
  });
}

Working Without Native Addons ​

Common Node.js libraries that rely on native addons and their edge alternatives:

LibraryNative DependencyEdge Alternative
bcryptC++ bindingbcryptjs (pure JS) or Web Crypto
sharplibvipsCloudflare Images API, or external service
canvasCairoSVG generation, or external service
pg / mysql2TCP socketsHTTP-based drivers (Neon, PlanetScale)
crypto (Node)OpenSSLWeb Crypto API
zlibnative zlibDecompressionStream / CompressionStream
better-sqlite3native SQLiteCloudflare D1, Turso

Web Crypto API for Common Operations ​

typescript
// Hashing
async function sha256(data: string): Promise<string> {
  const encoder = new TextEncoder();
  const hashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(data));
  return Array.from(new Uint8Array(hashBuffer))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
}

// HMAC signing (for JWT, webhooks)
async function hmacSign(
  secret: string,
  data: string
): Promise<string> {
  const encoder = new TextEncoder();
  const key = await crypto.subtle.importKey(
    'raw',
    encoder.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign']
  );
  const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
  return btoa(String.fromCharCode(...new Uint8Array(signature)));
}

// JWT verification at the edge
async function verifyJwt(
  token: string,
  secret: string
): Promise<Record<string, unknown> | null> {
  const parts = token.split('.');
  if (parts.length !== 3) return null;

  const [headerB64, payloadB64, signatureB64] = parts;

  // Verify signature
  const data = `${headerB64}.${payloadB64}`;
  const expectedSig = await hmacSign(secret, data);

  // Constant-time comparison
  const sig = signatureB64.replace(/-/g, '+').replace(/_/g, '/');
  if (sig !== expectedSig) return null;

  // Decode payload
  const payload = JSON.parse(atob(payloadB64));

  // Check expiration
  if (payload.exp && payload.exp < Date.now() / 1000) {
    return null;
  }

  return payload;
}

// AES encryption/decryption
async function encryptAES(
  plaintext: string,
  keyData: ArrayBuffer
): Promise<{ iv: string; ciphertext: string }> {
  const key = await crypto.subtle.importKey(
    'raw', keyData, 'AES-GCM', false, ['encrypt']
  );
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const encoder = new TextEncoder();
  const encrypted = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv },
    key,
    encoder.encode(plaintext)
  );

  return {
    iv: btoa(String.fromCharCode(...iv)),
    ciphertext: btoa(String.fromCharCode(...new Uint8Array(encrypted))),
  };
}

// UUID generation
function generateUUID(): string {
  return crypto.randomUUID();
}

Streaming Responses ​

Edge runtimes excel at streaming — processing data as it arrives without buffering the entire response in memory:

typescript
// Stream a large response from origin through the edge
async function streamProxy(request: Request): Promise<Response> {
  const originResponse = await fetch('https://origin.example.com/large-file');

  // Transform the stream at the edge
  const transformStream = new TransformStream({
    transform(chunk, controller) {
      // Process each chunk (e.g., inject headers, modify HTML)
      controller.enqueue(chunk);
    },
  });

  originResponse.body?.pipeTo(transformStream.writable);

  return new Response(transformStream.readable, {
    headers: originResponse.headers,
  });
}

// Server-Sent Events from the edge
function createSSEStream(): Response {
  const encoder = new TextEncoder();
  let interval: ReturnType<typeof setInterval>;

  const stream = new ReadableStream({
    start(controller) {
      let count = 0;
      interval = setInterval(() => {
        const data = JSON.stringify({ count: count++, time: Date.now() });
        controller.enqueue(
          encoder.encode(`data: ${data}\n\n`)
        );

        if (count > 100) {
          controller.close();
          clearInterval(interval);
        }
      }, 1000);
    },
    cancel() {
      clearInterval(interval);
    },
  });

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

Database Access from the Edge ​

Traditional database drivers use TCP sockets, which are not available at the edge. Edge-compatible databases use HTTP:

typescript
// Neon (PostgreSQL over HTTP)
import { neon } from '@neondatabase/serverless';

const sql = neon(process.env.DATABASE_URL!);

export default {
  async fetch(request: Request): Promise<Response> {
    const users = await sql`SELECT * FROM users LIMIT 10`;
    return Response.json(users);
  },
};

// PlanetScale (MySQL over HTTP)
import { connect } from '@planetscale/database';

const conn = connect({
  host: process.env.PLANETSCALE_HOST,
  username: process.env.PLANETSCALE_USERNAME,
  password: process.env.PLANETSCALE_PASSWORD,
});

export default {
  async fetch(request: Request): Promise<Response> {
    const results = await conn.execute('SELECT * FROM users LIMIT 10');
    return Response.json(results.rows);
  },
};

// Cloudflare D1 (SQLite at the edge)
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const { results } = await env.DB.prepare(
      'SELECT * FROM users WHERE id = ?'
    ).bind(1).all();
    return Response.json(results);
  },
};

// Turso (LibSQL, distributed SQLite)
import { createClient } from '@libsql/client/web';

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

export default {
  async fetch(request: Request): Promise<Response> {
    const result = await client.execute('SELECT * FROM users LIMIT 10');
    return Response.json(result.rows);
  },
};

Edge Cases and Failure Modes ​

1. CPU Time Exceeded ​

typescript
// BAD: CPU-intensive operation at the edge
function fibonacci(n: number): number {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

export default {
  async fetch(): Promise<Response> {
    const result = fibonacci(45); // Takes ~10 seconds of CPU
    // ERROR: "Worker exceeded CPU time limit"
    return Response.json({ result });
  },
};

// FIX: Offload to origin or use efficient algorithm
function fibonacciEfficient(n: number): number {
  let a = 0, b = 1;
  for (let i = 0; i < n; i++) {
    [a, b] = [b, a + b];
  }
  return a;
}

2. Subrequest Limits ​

typescript
// BAD: Fan-out exceeding subrequest limit
export default {
  async fetch(request: Request): Promise<Response> {
    const urls = Array.from({ length: 100 }, (_, i) =>
      `https://api.example.com/item/${i}`
    );

    // On Cloudflare free plan: limit is 50 subrequests
    const results = await Promise.all(
      urls.map(url => fetch(url).then(r => r.json()))
    );
    // ERROR after 50th fetch: "Too many subrequests"

    return Response.json(results);
  },
};

// FIX: Batch API calls or use pagination
export default {
  async fetch(request: Request): Promise<Response> {
    // Single batch request instead of 100 individual ones
    const results = await fetch('https://api.example.com/items?ids=0-99')
      .then(r => r.json());
    return Response.json(results);
  },
};

3. Memory Limits with Large Payloads ​

typescript
// BAD: Buffering entire response in memory
export default {
  async fetch(): Promise<Response> {
    const response = await fetch('https://origin.example.com/huge-file');
    const data = await response.arrayBuffer(); // 500MB = OOM
    // Process...
    return new Response(data);
  },
};

// FIX: Use streaming
export default {
  async fetch(): Promise<Response> {
    const response = await fetch('https://origin.example.com/huge-file');
    // Stream through without buffering
    return new Response(response.body, {
      headers: response.headers,
    });
  },
};

4. Global State Pitfalls ​

typescript
// WARNING: Global state persists between requests in the same isolate
// but is NOT shared between isolates or POPs

let requestCount = 0; // This persists between requests in the same isolate

export default {
  async fetch(): Promise<Response> {
    requestCount++; // NOT a reliable counter!
    // Different POPs have different isolates with different counts
    // Same POP may create new isolates at any time
    return Response.json({ count: requestCount });
  },
};

// FIX: Use external state (KV, Durable Objects, database)
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const count = parseInt(await env.KV.get('request-count') || '0') + 1;
    await env.KV.put('request-count', String(count));
    return Response.json({ count });
  },
};

War Story

The bcrypt Import That Broke Production

A team deployed an authentication edge function that imported bcrypt for password hashing. The function worked locally (Node.js) but failed at deploy time because bcrypt depends on native C++ bindings. The build silently fell back to a JavaScript polyfill that was 100x slower — each password hash took 2 seconds of CPU time, exceeding the edge function's CPU limit.

The fix was switching to Web Crypto API for password verification using PBKDF2, which is hardware-accelerated and completes in <5ms at the edge. For the password hashing itself (higher cost desired), they kept it on the origin server.

War Story

The Edge Function That Ran Out of Subrequests

A product listing page ran on Cloudflare Workers. Each product needed enrichment: price from a pricing service, inventory from a stock service, and reviews from a review service. With 50 products on the page, that was 150 subrequests — exceeding the 50-request limit.

The fix was a two-part solution: (1) implement a GraphQL gateway at the origin that accepted batch product enrichment requests, reducing 150 calls to 1, (2) cache enriched products at the edge in KV with a 60-second TTL, so most page loads required zero subrequests.

Performance Characteristics ​

Operation Costs at the Edge ​

OperationTime (ms)Notes
String manipulation0.001-0.01Very fast
JSON.parse (1KB)0.01Fast
JSON.parse (100KB)0.5Moderate
JSON.parse (1MB)5Approaches CPU limits
SHA-256 hash (1KB)0.05Web Crypto, fast
HMAC-SHA256 sign0.1Web Crypto
AES-256 encrypt (1KB)0.1Web Crypto, hardware
JWT verify0.2Signature + decode
KV read1-5Edge cache hit
D1 query2-10Local SQLite
fetch() to origin20-200Network round-trip

Memory Usage Patterns ​

Available Memory=Limit−Runtime Overhead−Script Size

For a typical Worker:

  • Limit: 128MB
  • Runtime overhead: ~10MB
  • Script size: 1-5MB
  • Available for data: ~113-117MB

A 113MB budget holds:

  • ~1 million small JSON objects (~100 bytes each)
  • ~15,000 medium objects (~8KB each)
  • ~113 large responses (1MB each)

Mathematical Foundations ​

Isolate Density Model ​

The number of isolates that can run concurrently on a single edge node:

Nisolates=min(MtotalMper_isolate,CtotalCper_request×RPS)

Where:

  • Mtotal = total node memory (e.g., 64GB)
  • Mper_isolate = memory per isolate (~128KB-128MB)
  • Ctotal = total CPU cores
  • Cper_request = CPU ms per request
  • RPS = requests per second per isolate

For 64GB RAM, 128KB per isolate, 16 cores:

Nisolates=min(64×109128×103,…)=500,000 (memory-limited)

This is why edge platforms can serve millions of requests per second from a single node.

Decision Framework ​

Compatibility Checklist ​

Before deploying to the edge, verify each dependency:

Dependency CategoryEdge Compatible?Alternative
Pure JavaScript librariesYesUse directly
Web API-based librariesYesUse directly
Node.js cryptoPartialWeb Crypto API
Node.js bufferPolyfilledUse Uint8Array
Node.js fsNoKV, R2, fetch
Node.js netNoHTTP-based clients
Native addonsNoPure JS alternatives
Node.js http/httpsNofetch API
process.envVariesenv bindings

Migration Strategy ​

Advanced Topics ​

Polyfilling Node.js APIs ​

Some edge platforms provide Node.js API compatibility layers:

typescript
// Cloudflare Workers: Node.js compatibility flags
// wrangler.toml:
// compatibility_flags = ["nodejs_compat"]

// Now available:
import { Buffer } from 'node:buffer';
import { createHash } from 'node:crypto';
import { AsyncLocalStorage } from 'node:async_hooks';

// But NOT available (even with compat flag):
// import fs from 'node:fs';
// import net from 'node:net';
// import child_process from 'node:child_process';

Testing Edge Functions Locally ​

typescript
// Miniflare: Local Cloudflare Workers simulator
// npm install miniflare

// wrangler.toml
// [dev]
// port = 8787

// Run locally:
// npx wrangler dev

// Or use Miniflare directly:
import { Miniflare } from 'miniflare';

const mf = new Miniflare({
  script: `
    export default {
      async fetch(request, env) {
        return new Response("Hello from edge!");
      }
    }
  `,
  modules: true,
  kvNamespaces: ['MY_KV'],
  d1Databases: ['MY_DB'],
});

const response = await mf.dispatchFetch('http://localhost:8787/');
console.log(await response.text()); // "Hello from edge!"

Edge-Compatible Library Patterns ​

typescript
// Pattern: Isomorphic library that works on Node.js and edge

// Detect runtime
function getRuntime(): 'node' | 'edge' | 'browser' {
  if (typeof globalThis.process !== 'undefined' && globalThis.process.versions?.node) {
    return 'node';
  }
  if (typeof globalThis.EdgeRuntime !== 'undefined' ||
      typeof globalThis.caches !== 'undefined') {
    return 'edge';
  }
  return 'browser';
}

// Adapter pattern for cross-runtime compatibility
interface StorageAdapter {
  get(key: string): Promise<string | null>;
  set(key: string, value: string, ttl?: number): Promise<void>;
  delete(key: string): Promise<void>;
}

// Node.js adapter
class RedisStorageAdapter implements StorageAdapter {
  constructor(private readonly redis: RedisClient) {}
  async get(key: string) { return this.redis.get(key); }
  async set(key: string, value: string, ttl?: number) {
    if (ttl) await this.redis.set(key, value, 'EX', ttl);
    else await this.redis.set(key, value);
  }
  async delete(key: string) { await this.redis.del(key); }
}

// Edge adapter
class KVStorageAdapter implements StorageAdapter {
  constructor(private readonly kv: KVNamespace) {}
  async get(key: string) { return this.kv.get(key); }
  async set(key: string, value: string, ttl?: number) {
    await this.kv.put(key, value, ttl ? { expirationTtl: ttl } : undefined);
  }
  async delete(key: string) { await this.kv.delete(key); }
}

Key Takeaway

Edge runtime constraints are the price of sub-5ms cold starts and global distribution. The key constraint to internalize is: no filesystem, no TCP sockets, limited CPU time. Design your edge functions to be lightweight request transformers and cache-aware proxies. Heavy computation, large file processing, and complex database operations belong on the origin. The edge handles the last mile — authentication, caching, personalization, and routing.

Cross-References ​

"What I cannot create, I do not understand." — Richard Feynman