🍱 Lunchbox Hands

testing

How to Test File Upload Limits: Boundaries, 413s, and Exact-Size Files

A practical developer guide to testing file upload limits correctly — nginx client_max_body_size, cloud function payload caps, S3 multipart thresholds, the MB vs MiB unit trap, and how to generate exact-size test files in any format.

Upload limits are one of those things that feel obvious until they aren’t. You set client_max_body_size 10m in nginx, test it with a file under 10 MB, ship it — and then someone uploads a 10.001 MB file and gets a blank page instead of a useful error message. Testing upload limits correctly means understanding every layer that can reject an oversized payload and hitting the boundary precisely, not just staying safely under it.

Where upload limits actually live

There are at least four distinct places a large upload can be rejected, and they don’t always agree with each other.

nginx client_max_body_size — The most common culprit. nginx rejects the request body before it ever reaches your app, returning 413 Request Entity Too Large. The default is 1m (1 MiB — more on units below). If you configure this in a server block but forget a nested location block, the limit silently reverts to the default for that route.

Cloud function payload limits — Cloudflare Workers caps incoming request bodies at 100 MB. AWS Lambda fronted by API Gateway has a hard 10 MB limit on the request payload, regardless of what your application code accepts. Vercel Edge Functions cap at 4.5 MB. These are infrastructure ceilings you can’t override from application code.

S3 and multipart upload thresholds — If you’re accepting uploads directly to S3 via pre-signed URLs, S3’s minimum part size for multipart is 5 MiB (except the last part). Below that, most SDKs use a single PUT; above it, they switch to multipart. If your client-side upload code doesn’t handle the multipart path, large uploads fail silently or with an SDK error the frontend swallows.

Application-level checks — Your own code: if (file.size > 10_000_000) return 400. This is the most controllable layer, but it’s the last one the request actually reaches. A rejection from nginx never touches it.

The unit problem: MB vs MiB

This is where a lot of production incidents quietly originate. A megabyte (MB) is 1,000,000 bytes. A mebibyte (MiB) is 1,048,576 bytes — about 4.86% larger.

nginx’s client_max_body_size 10m is in mebibytes: 10 × 1,048,576 = 10,485,760 bytes. If your UI tells users “maximum file size: 10 MB” and validates against 10,000,000 bytes, a file between 10,000,001 and 10,485,760 bytes will pass your client-side check but get rejected by nginx with a 413. The user sees nothing useful.

To convert accurately between the two before you write any validation logic, the Bytes / Filesize Converter handles both decimal (MB) and binary (MiB) — paste in any nginx or platform limit and get the exact byte count to use in your code.

Testing the boundary correctly

The goal is three specific cases:

  1. Just under the limit — a file at limit - 1 byte. Must succeed.
  2. Exactly at the limit — a file at exactly limit bytes. Most systems reject at > limit, so this should succeed. Verify your stack does what you think it does.
  3. Just over the limit — a file at limit + 1 byte. Must return a proper error.

For a 10 MiB nginx limit:

  • 10,485,759 bytes → should succeed
  • 10,485,760 bytes → should succeed (confirm)
  • 10,485,761 bytes → should return 413

What the error response looks like matters as much as the status code. nginx’s default 413 page is a raw HTML string. If your frontend doesn’t handle a non-JSON response from an endpoint it expects to return JSON, you’ll get an unhandled rejection or an opaque “invalid response” error instead of “file too large.” Test the error path as carefully as the happy path.

Generating exact-size test files

The classic shell approach:

# create a 10,485,761-byte sparse file
truncate -s 10485761 toolarge.bin

truncate is fast and accurate for size testing. dd if=/dev/urandom also works but is slow for anything over a few MB. Both produce binary blobs — fine for confirming a 413, but useless if your endpoint also validates MIME type or inspects magic bytes.

For testing specific formats at exact sizes, the Sample File Generator does the job in your browser: generate a genuinely valid file — PDF, PNG, JPG, WebP, GIF, BMP, SVG, ZIP, TAR, GZIP, CSV, JSON, XML, YAML, SQL, Markdown, TXT, TSV, LOG, or binary — at any exact byte count up to 250 MB, no install required. Enter the byte count you calculated, pick the format your endpoint expects, and download.

The tool uses decimal units (1 MB = 1,000,000 bytes), so for MiB-based limits, calculate the byte count first — either mentally or with the Bytes Converter — then enter the raw number.

MIME type validation

Upload limits aren’t only about size. Most endpoints also check the MIME type of the incoming file, either from the Content-Type request header or by inspecting the file’s magic bytes directly.

This intersects with size testing: when you generate boundary-size test files, the MIME type needs to be correct too. A binary blob renamed to .pdf will fail MIME validation before the size check even runs, giving you a false pass on the boundary — you think the size test passed, but the request was rejected for an entirely different reason.

Genuine format files matter here. A PDF that actually opens as a PDF and is exactly 10,485,761 bytes is a more honest test than a renamed blob.

Common mistakes

  • Testing only the happy path. Not testing just-over-the-limit, and not verifying the response code and body when the limit is actually hit.
  • Treating MB and MiB as interchangeable. Audit every limit in your stack in raw bytes before writing any validation.
  • Not knowing which layer rejected the request. Set your app limit to 500 MB and test at 11 MiB to confirm nginx is the actual constraint — not your code, not the CDN.
  • Ignoring multipart form overhead. An HTML <form enctype="multipart/form-data"> adds several hundred bytes of boundary metadata on top of the file payload. A file sitting right at the limit can exceed it after encoding.
  • Not testing the error UX. A 413 that renders as a blank white page or a JSON parse error in the browser console is a bug, even if the server behaved correctly.

A repeatable boundary test workflow

  1. List every layer with a configured limit: CDN, reverse proxy, cloud function, app code.
  2. Convert every limit to exact bytes. Be explicit about MB (decimal) vs MiB (binary).
  3. Generate test files at limit - 1, limit, and limit + 1 bytes in the MIME type the endpoint actually accepts.
  4. Confirm a success response on the first two and a 413 (or your defined error code) on the third.
  5. Verify the frontend handles the error response gracefully — correct status, readable message, no unhandled promise rejections.

Boundary testing is not glamorous. But it’s exactly the kind of thing that catches you at 2 AM when a user tries to upload something marginally larger than whatever file you happened to test with.