1082 words
5 minutes

Cloudflare Workers 413: Find the Request Body Limit Before Rewriting Uploads

2026-08-12
DevOps
Cloudflare
/
DevOps
/
Troubleshooting
/
Deployment

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:

LayerWhat to inspectWhy it matters
Workers edgeAccount-plan request body limitThe request may be rejected before fetch() runs
Cloudflare zone or APIMaximum upload size or API upload limitA zone setting can be lower than the plan maximum
OriginNginx, Caddy, framework, or application body limitThe 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:

Terminal window
curl -i -X POST -H 'Content-Type: application/octet-stream' --data-binary @sample-120mb.bin https://api.example.com/upload

Record:

  1. The exact hostname and path.
  2. The request size in bytes.
  3. Whether the request is proxied through Cloudflare.
  4. The response status, headers, and body.
  5. 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:

TestExpected observation
Small JSON bodyWorker parses and returns the normal response
Just above app limitWorker returns your documented 413
Large body with no Worker invocationEdge or zone setting is rejecting it
Worker forwards, origin returns 413Origin body limit is the blocker
Direct R2 PUTFile 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:

Cloudflare Workers limits

Cloudflare Error 413

Cloudflare R2 presigned URLs

Cloudflare R2 S3 API upload methods

Cloudflare Workers 413: Find the Request Body Limit Before Rewriting Uploads
https://laplusda.com/en/posts/cloudflare-workers-413-request-body/
Author
Zero
Published at
2026-08-12
License
CC BY-NC-SA 4.0
Was this article useful?

Report a typo or broken link, or suggest a related topic.