Memory and CPU Limits Across Edge Providers
Edge runtimes impose hard resource boundaries enforced at the JavaScript engine level, not the OS level. Exceeding memory causes an immediate OOM kill. Exceeding the CPU budget (where one exists) triggers a hard timeout error. Neither condition produces a graceful error page—the platform returns a 502 or 503 to the client. Understanding exactly where each provider draws those lines is a prerequisite for capacity planning.
This guide is part of Edge Runtime Fundamentals & Platform Constraints. It maps the two ceilings every edge workload must respect — resident memory and synchronous CPU time — across all three major providers.
Provider Limits
Cloudflare Workers
- Memory: 128 MB per isolate (hard cap)
- CPU: 10 ms synchronous CPU per request on the free tier; 30 s by default on the Workers Paid plan, configurable up to 5 minutes. I/O wait (outbound
fetch, KV reads, DO calls) does not consume this budget. - Wall-clock: 30 s
- Bundle size: 1 MB uncompressed script
The CPU budget is the defining constraint. A synchronous SHA-256 hash of a 10 MB buffer, or a regex evaluated against a large string, can exhaust 10 ms without any network I/O. Exceeding the CPU quota returns a Worker threw exception error (Cloudflare error code 1101). For the diagnostic and remediation workflow specific to this failure, see avoiding CPU time-limit errors in Cloudflare Workers.
Vercel Edge Middleware
- Memory: 128 MB per invocation (hard cap)
- CPU: No separate CPU time budget; wall-clock limit applies
- Wall-clock: 1000 ms for
middleware.ts - Bundle size: 1 MB uncompressed
Vercel does not enforce a separate synchronous CPU limit. The 1000 ms wall-clock includes all computation and I/O. This is more permissive than Cloudflare for CPU-bound operations but less permissive for long-running I/O chains.
Netlify Edge Functions
- Memory: 512 MB per invocation
- CPU: No separate CPU budget
- Wall-clock: 50 s
- Bundle size: 20 MB
Netlify’s Deno-based runtime is the most permissive in terms of memory and execution time, making it a reasonable choice for transformation-heavy middleware that cannot be offloaded to origin. For a focused breakdown of the 128 MB versus 512 MB difference and the distinct OOM signatures, see comparing memory limits: Netlify vs Vercel Edge.
Summary Table
| Provider | Memory | CPU budget | Wall-clock | Bundle |
|---|---|---|---|---|
| Cloudflare Workers | 128 MB | 10 ms (free) / 30 s default, up to 5 min (paid) | 30 s | 1 MB |
| Vercel Edge Middleware | 128 MB | — | 1000 ms | 1 MB |
| Netlify Edge Functions | 512 MB | — | 50 s | 20 MB |
Streaming to Avoid OOM
Buffering large payloads is the most common cause of OOM kills on 128 MB platforms. Use TransformStream to process data in chunks and flush immediately:
export async function edgeHandler(request: Request): Promise<Response> {
const startTime = performance.now();
const response = await fetch('https://api.origin/data');
if (!response.ok || !response.body) {
return new Response('Upstream failed', { status: 502 });
}
// Reject payloads that would exceed safe memory headroom
const contentLength = response.headers.get('content-length');
if (contentLength && parseInt(contentLength, 10) > 80 * 1024 * 1024) {
return new Response('Payload exceeds edge memory budget', { status: 413 });
}
// Stream without buffering; enforce a wall-clock guard per chunk
const transformStream = new TransformStream({
transform(chunk, controller) {
if (performance.now() - startTime > 900) {
// Approaching wall-clock limit; abort the stream
controller.error(new Error('Execution budget approaching limit'));
return;
}
controller.enqueue(chunk);
},
});
return new Response(response.body.pipeThrough(transformStream), {
headers: response.headers,
});
}
Lazy Module Loading
Static top-level imports parse and compile at isolate initialization time. Deferred import() avoids this overhead for code paths that are rarely hit:
type HeavyProcessor = { process: (data: ArrayBuffer) => Promise<Uint8Array> };
let processorInstance: HeavyProcessor | null = null;
let initPromise: Promise<HeavyProcessor> | null = null;
export async function getProcessor(): Promise<HeavyProcessor> {
if (processorInstance) return processorInstance;
if (!initPromise) {
initPromise = (async () => {
try {
const { HeavyProcessor } = await import('./heavy-processor');
processorInstance = new HeavyProcessor();
return processorInstance;
} catch (error) {
initPromise = null; // Allow retry on transient failures
throw error;
}
})();
}
return initPromise;
}
For the relationship between module size and cold-start latency, see Managing Cold Starts in Serverless Environments.
Observability
Platform-native CPU time headers are not consistently available. Use performance.now() to instrument critical paths:
const start = performance.now();
const result = await heavyOperation();
const elapsed = performance.now() - start;
if (elapsed > 20) {
console.warn(JSON.stringify({
level: 'warn',
message: 'Heavy operation approaching CPU budget',
elapsedMs: elapsed.toFixed(2),
}));
}
For local load testing that simulates production memory caps:
# Cap Node.js heap to simulate 128 MB edge limit
node --max-old-space-size=128 dist/server.js
Workload Decision Matrix
| Workload | Memory Profile | Recommended Strategy |
|---|---|---|
| JWT validation, auth routing | < 5 MB | Deploy to edge; CPU budget not a concern |
| API aggregation / BFF patterns | 20–50 MB | Edge with streaming; cache aggressively |
| Large JSON transformation | > 80 MB | Offload to regional serverless (Node.js, Python) |
| Static asset manipulation | < 15 MB | Edge with CDN caching |
| Database query + render | Variable | Serverless or containerized; not edge |
When average CPU time on Cloudflare exceeds 25 ms, or sustained memory usage exceeds 80 MB on any 128 MB platform, route to a regional serverless function. The hybrid model—edge for routing and stateless transforms, serverless for compute—preserves low latency for user-facing requests while handling heavy operations in an elastic environment.
Common Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
502/504 with no error body on Vercel/CF |
OOM kill from buffering a large payload in heap | Stream with TransformStream; reject oversized bodies via content-length |
| Cloudflare error 1101 under load | Synchronous CPU (hash, regex, parse) exceeds the 10 ms free budget | Offload compute or upgrade to a paid CPU tier; see the CPU time-limit guide |
| Works locally, OOMs in production | vercel dev / netlify dev do not enforce memory caps |
Run under node --max-old-space-size=128 before deploying |
| Slow first request despite small handler | Large static imports parsed at isolate init, inflating both heap and cold start | Defer rarely-hit modules with dynamic import() |
| Intermittent Netlify timeouts under concurrency | Shared Deno worker-pool pressure, not a single-function limit | Cap per-request memory and avoid unbounded cache writes |
Resource-Boundary Checklist
Validate each ceiling before promoting middleware to production:
1. Confirm the memory headroom
Subtract runtime and framework overhead (roughly 20–40 MB) from the cap and verify peak heap stays below it under load.
2. Stream anything large
Replace synchronous buffering with TransformStream, and reject payloads above a safe content-length threshold with a 413.
3. Budget synchronous CPU
On Cloudflare, keep per-request synchronous work under the active CPU budget; move hashing and heavy parsing off the hot path.
4. Defer cold modules
Load rarely-hit code with dynamic import() so it never inflates initialization heap or parse time.
- Large payloads streamed, not buffered, with a
413 - Heavy modules deferred behind dynamic
- Local runs capped with
--max-old-space-size
Frequently Asked Questions
What happens when an edge function exceeds its memory limit?
The platform issues an immediate OOM kill at the JavaScript engine level and returns a 502 or 504 with no application error body. There is no graceful catch — the isolate is terminated mid-request, so the only defense is staying below the cap by streaming rather than buffering.
Does outbound fetch count against Cloudflare's CPU budget?
No. The CPU budget measures synchronous compute only. Time spent waiting on outbound fetch, KV reads, or Durable Object calls is I/O wait and does not consume the 10 ms free-tier or paid CPU budget. Only synchronous work like hashing or regex evaluation does.
Which provider has the most permissive limits?
Netlify Edge Functions, with 512 MB of memory and a 50 s wall-clock on the Deno runtime, are the most permissive. Cloudflare’s 10 ms free-tier CPU budget is the strictest. Vercel Edge sits between them with 128 MB and a 1000 ms wall-clock that bundles compute and I/O together.
How do I simulate the 128 MB cap locally?
Neither vercel dev nor netlify dev enforces memory limits, so run your build under node --max-old-space-size=128 dist/server.js and drive load against it. This surfaces handlers that approach the ceiling before they OOM in production.
When should I move a workload off the edge entirely?
Route to a regional serverless function when average Cloudflare CPU time exceeds 25 ms or sustained memory passes 80 MB on any 128 MB platform. Keep routing and stateless transforms at the edge and push heavy compute to an elastic environment.
Conclusion
Edge memory and CPU limits are not soft suggestions—they are hard enforcement points that kill your request. Cloudflare’s 10 ms free-tier CPU budget is among the strictest constraints in the industry; Netlify’s 512 MB memory and 50 s wall-clock are the most permissive. Design middleware that stays well below these ceilings: stream large payloads, defer heavy modules, and route CPU-intensive work to serverless functions. For platform-specific comparison of memory allocation strategies see Comparing Memory Limits: Netlify vs Vercel Edge.