Platform

Cloudflare Workers limits for a CORS proxy

A CORS proxy is a thin layer around one call: fetch a URL, check it's safe, send back the response with the right headers attached. On corsproxy.dev that layer runs as a single Cloudflare Worker, which means every proxied request shares the same CPU time, memory, and subrequest allowances as any other Worker on the platform. Most traffic never comes close to any of them, but the numbers still shaped decisions we made about buffering, the DNS safety check, and how much one request is allowed to do at once.

· ~7 min read

CPU time only counts code, not the wait

Cloudflare's Workers limits documentation defines CPU time as how long the CPU spends executing a Worker's own code; time spent waiting on a fetch() call, a KV read, or a database query doesn't count toward it. The Workers Free plan gets 10 ms of CPU time per request, and Workers Paid gets 30 seconds by default, raisable to 5 minutes (300,000 ms) in the Wrangler configuration or the dashboard.

That split matters more for a proxy than for most Workers, because almost none of its wall-clock time is CPU time. Parsing the query string, checking the API key, and assembling headers takes a few milliseconds; the rest of the request is spent waiting on a response from whatever host the caller pointed at. A slow upstream makes the proxied request take longer, but it doesn't push the Worker toward a CPU-time error, because that upstream wait was never counted in the first place. The same docs note there's no overall duration limit for an HTTP-triggered Worker: as long as the client stays connected, the Worker can keep making subrequests and streaming a response. A hung upstream shows up to the caller as a slow or dropped connection, not as a Cloudflare-side timeout.

One proxied call costs up to three subrequests

Cloudflare counts a subrequest as any request a Worker makes with the Fetch API or to a Cloudflare service like KV, R2, or D1. The limit is 50 subrequests per invocation on Free, and 10,000 by default on Paid, configurable up to 10 million per Worker.

A single proxied request on corsproxy.dev can use three of those before any response leaves our edge. Before the target is fetched, we resolve its hostname through Cloudflare's security DNS resolver over DNS-over-HTTPS, as described in how a CORS proxy works, and how it can be abused: one lookup for A records, one for AAAA, fired together rather than one after the other. That's two subrequests before the third, the actual fetch to the target, even runs. The result of that DNS check is cached by hostname for five minutes, but the cache is an ordinary in-memory map living inside one Worker isolate, not a Cloudflare-wide store; a cold isolate re-resolves a host it's never seen, and the two DoH lookups run again.

Three subrequests is nowhere near either limit, which is really the point: the ceiling would only start to matter for a Worker that chains several upstream calls per client request. corsproxy.dev doesn't; it calls exactly one target per request and returns an upstream redirect to the caller instead of following it itself, a tradeoff covered in why CORS proxies don't follow redirects for you. The subrequest count stays flat regardless of how large or slow that one target's response is.

128 MB per isolate, and why we don't buffer up to it

Memory is capped at 128 MB per isolate, covering the JavaScript heap and any WebAssembly allocations, on both Free and Paid. That limit is per isolate, not per request: a single isolate handles many concurrent invocations, and they all draw from the same 128 MB. Cloudflare's guidance for working inside it is direct: stream request and response bodies with a TransformStream instead of buffering an entire payload in memory, and avoid holding large objects for longer than necessary. Exceed the limit and the runtime returns Error 1102, lets in-flight requests finish, and starts a fresh isolate for what comes next.

corsproxy.dev does buffer the upstream response rather than stream it, so that an oversized body fails cleanly with a 502 instead of arriving cut off partway through, a behavior we cover in when not to use a CORS proxy and in Server-Sent Events and CORS. The cap on that buffer is 10 MB in each direction, well under the 128 MB ceiling, precisely because that ceiling is shared. An isolate serving several concurrent proxy requests at once has to fit all of their buffered bodies in the same 128 MB; sizing the per-request cap at a fraction of the isolate limit, rather than near it, is what keeps one large response from crowding out everything else running on the same isolate.

Six connections waiting on headers, used two at a time

Each Worker invocation can have at most six connections simultaneously waiting for response headers; fetch() calls, KV and R2 operations, and outbound WebSockets all draw from the same pool while they're in that "waiting" state. Once a connection's headers arrive, it stops counting, so the limit is about how many requests can be in flight with no answer yet, not how many a Worker can make in total.

The two parallel DoH lookups behind the DNS safety check use two of those six at once; the fetch to the target runs afterward, once the check has a verdict, so it's never competing with the DNS calls for the same slots. A Pro request with ttl set adds a Cache API read, also counted while it's pending. None of that gets close to six. The limit would bind harder on a Worker built to fan a single client request out across several upstreams concurrently, which is a different shape of proxy than corsproxy.dev: ours calls one target per request, by design, and the SSRF checks around that single call are the reason it stays that way.

The platform ceiling isn't where our limits come from

None of the numbers above explain why a Free API key gets 500 requests a day and 250 MB of bandwidth, or why Pro stops at 20,000 and 10 GB, as laid out in rate limits and quotas on corsproxy.dev, explained. Those are business limits we enforce ourselves, tracked per account in a Durable Object so the count is exact and consistent across every Cloudflare location, the approach we picked over Workers KV for exactly that reason in Cloudflare KV vs D1 vs Durable Objects, in production. The platform would let a single Worker invocation do far more, in CPU time, subrequests, and memory, than any individual request to corsproxy.dev ever does. The limits that actually show up for a customer come from the quota we chose to put on top, not from the infrastructure underneath it.