Cloudflare Workers 413: Find the Request Body Limit Before Rewriting Uploads
A 413 Request Entity Too Large response does not automatically mean that your Worker ran out of memory. The request can be rejected before the Worker executes, by a zone upload setting, or later by the origin behind the Worker. Find the layer that returned the 413 before changing application code.
For a Worker endpoint, the practical decision is:
- Keep small JSON or form requests behind the Worker and validate them there.
- Chunk a large upload when the application needs to process pieces.
- For browser-to-object-storage uploads, let the Worker authorize a short-lived R2 presigned URL and upload directly to R2.
Streaming a body inside the Worker cannot bypass an edge request-size limit that rejects the request before your handler sees it.
Start with the three possible limits
There are several limits that are easy to conflate:
| Layer | What to inspect | Why it matters |
|---|---|---|
| Workers edge | Account-plan request body limit | The request may be rejected before fetch() runs |
| Cloudflare zone or API | Maximum upload size or API upload limit | A zone setting can be lower than the plan maximum |
| Origin | Nginx, Caddy, framework, or application body limit | The Worker may forward the request successfully, but the origin can reject it |
Cloudflare’s current Workers limits list a maximum request body of 100 MB for Free and Pro, 200 MB for Business, and 500 MB by default for Enterprise. Those values are plan-dependent and can change, so treat the Workers limits page as the source of truth rather than copying a number into an application constant.
Cloudflare’s 413 troubleshooting page describes a second boundary: the zone’s Maximum Upload Size can reduce the effective upload limit, and Cloudflare API uploads have plan-dependent limits. An origin can impose a third limit after the request passes through the edge.
Identify who returned the 413
Reproduce the failure with a known-size test file and preserve the response headers:
curl -i -X POST -H 'Content-Type: application/octet-stream' --data-binary @sample-120mb.bin https://api.example.com/uploadRecord:
- The exact hostname and path.
- The request size in bytes.
- Whether the request is proxied through Cloudflare.
- The response status, headers, and body.
- Whether a Worker invocation, origin access log, or application log was created.
If there is no Worker invocation and no origin log, the rejection is probably upstream of your handler. If the Worker log exists but the origin log does not, inspect the Worker route, request forwarding, and any explicit size guard. If the origin log shows the request and returns 413, fix the origin’s body limit instead of changing Cloudflare’s plan.
Do not use only a browser upload for this test. Browsers may retry, send multipart boundaries, or hide the exact response body. A repeatable curl request gives you a size boundary you can move up and down.
Keep the Worker handler bounded
For a small JSON endpoint, validate the declared content type and size before parsing:
const MAX_JSON_BYTES = 1024 * 1024;
export default { async fetch(request) { const contentLength = Number(request.headers.get('content-length') || 0);
if (contentLength > MAX_JSON_BYTES) { return Response.json( { error: 'Request body is too large for this endpoint.' }, { status: 413 }, ); }
if (request.headers.get('content-type')?.includes('application/json') !== true) { return new Response('Unsupported content type', { status: 415 }); }
const body = await request.json(); return Response.json({ received: body }); },};This application-level check is useful for a predictable API contract, but it is not a replacement for the edge limit. A missing or misleading Content-Length header is not proof that the body is small, so the handler still needs a safe parsing strategy for its endpoint.
Avoid calling request.arrayBuffer() or request.text() on unbounded uploads. Buffering a large body consumes Worker memory and can turn a request-size problem into a memory problem. If the application needs the body, stream it or cap the route before parsing.
Move browser uploads directly to R2
When the Worker only authenticates the user and stores a file, proxying the entire file through the Worker adds an unnecessary request hop. Cloudflare’s R2 docs support a presigned PUT URL for a single object:
// Server-side sketch: create a short-lived, content-type-bound PUT URL.const uploadUrl = await createPresignedPutUrl({ bucket: 'uploads', key: 'user-123/photo.png', contentType: 'image/png', expiresIn: 900,});
return Response.json({ uploadUrl });The browser then uploads directly:
const response = await fetch(uploadUrl, { method: 'PUT', headers: { 'Content-Type': file.type }, body: file,});
if (!response.ok) { throw new Error('R2 upload failed');}The signing code must stay on the server, and the URL should be scoped to one object, one operation, and a short expiry. Configure bucket CORS for the browser origin and treat the URL as a bearer token. If uploads need resumability or parallel parts, use R2’s multipart upload path rather than pushing one huge body through a Worker.
The R2 presigned URL documentation notes that presigned URLs support PUT for object uploads, while HTML form POST uploads are not currently supported. Pick the client method that matches the storage API instead of assuming every S3 upload example works unchanged.
After changing the upload path, exercise the built Worker route with the production-build request harness described in Cloudflare Workers createTestHarness(). The test should cover an accepted small request, an application-level 413, and a direct-storage upload failure.
Re-test the boundary, not only the happy path
Use files just below and just above the application limit, then test the Cloudflare and origin limits separately. A useful matrix looks like this:
| Test | Expected observation |
|---|---|
| Small JSON body | Worker parses and returns the normal response |
| Just above app limit | Worker returns your documented 413 |
| Large body with no Worker invocation | Edge or zone setting is rejecting it |
| Worker forwards, origin returns 413 | Origin body limit is the blocker |
| Direct R2 PUT | File bypasses the Worker request body path |
Keep the original response headers and logs with the deployment change. If a future plan or zone setting changes, those records show which layer changed rather than leaving a vague “Cloudflare upload failed” report.
FAQ
Q: Can a Worker stream a request larger than the Cloudflare request body limit?
A: No. Streaming helps the Worker avoid buffering a request it has already received; it cannot bypass an edge limit that rejects the request before the handler runs. Use chunking, a direct object-storage upload, or an adjusted limit where appropriate.
Q: Why does a 413 remain after I increase the Worker plan?
A: The rejecting layer may be the zone’s Maximum Upload Size, a Cloudflare API upload limit, or the origin server. Confirm whether the Worker and origin both logged the request, then check the limit at that layer.
Q: Is a presigned R2 URL safe to expose to a browser?
A: It is intended for client use, but it is a bearer token. Scope it to one operation and object, bind the expected content type when signing, use a short expiry, and configure CORS for the intended origins.
References:
Report a typo or broken link, or suggest a related topic.