How to Test File Upload Limits (N-1, N, N+1)
By Niks · Published · Updated
Tested on
- Windows 11 Home (10.0.22631), Git Bash with GNU coreutils 8.32
- curl 8.21.0, nginx 1.30.5 for Windows, Node.js v22.13.0
- Run on 29 September 2026
How to test file upload limits: send three files at every limit, one byte under it (N-1), exactly at it (N) and one byte over it (N+1). The first two should be accepted and the third should be rejected with a clear error. Do it at every layer that can refuse a request, because the smallest limit in the chain wins and the error often does not say which layer produced it.
The rest of this guide covers the layers to check, what “10 MB” means at each one, how much bigger the request is than the file, and a script you can run. Every number below comes from a command we ran, and vendor limits link to the vendor’s own documentation.
How to test file upload limits: three files per limit
A limit bug lives at the boundary. Testing with one small file and one huge file tells you the limit exists, not where it is. Testing N-1, N and N+1 tells you whether the check is < or <=, whether it counts bytes or rounds, and whether the limit is really the number in the documentation.
For a limit of N bytes, generate:
| File | Size | Expected result |
|---|---|---|
| N-1 | N - 1 bytes | Accepted |
| N | N bytes | Accepted (if the limit is inclusive) or rejected (if it is exclusive), and the same every time |
| N+1 | N + 1 bytes | Rejected with a clear error, such as HTTP 413 |
The status code for “the body is too large” is 413. RFC 9110 names it “Content Too Large” (older documents and nginx call it “Request Entity Too Large” or “Payload Too Large”) and says the server may close the connection. A correct rejection is a 413 or a validation message. A 500, a timeout, a hung progress bar or a file that is saved cut short are all bugs to report.
You can build the three files with any tool that writes an exact byte count (see create a dummy file of any size), or in the browser with the dummy files generator: set the unit to B and type the byte count.
Find every layer that enforces a limit
A request from a browser to your application can pass through several components, and each one may have its own maximum body size:
- Client-side checks. A
<input accept>or JavaScript size check. Easy to bypass, so never the only check, but worth testing because it decides what the user sees first. - CDN or proxy in front of you. Cloudflare’s 413 documentation lists a maximum upload size of 100 MB on the Free and Pro plans, 200 MB on Business, and up to 5 GB on Enterprise, and says the value can be adjusted from the zone’s Network page.
- Reverse proxy. nginx rejects bodies over
client_max_body_size, which defaults to1m. See fixing nginx 413. - Application runtime. PHP has
upload_max_filesize(default2M) andpost_max_size(default8M). See PHP upload limits. Other runtimes have their own settings, so look them up in that runtime’s documentation. - Framework validation and storage. A form request rule, a database column, an object store.
Write down the limit and its unit at each layer, then test the lowest one first. If the lowest is 1 MiB and the CDN allows 100 MB, a 5 MB file will fail at the proxy and you learn nothing about the layers behind it until you raise it.
Decimal or binary: which “10 MB” is the limit?
The word “MB” means 1,000,000 bytes to some tools and 1,048,576 bytes to others. Test both values, because the gap between them is 485,760 bytes for “10 MB”.
We tested this against nginx 1.30.5 with two different limits on the same server. client_max_body_size 10m; is binary (10 MiB). client_max_body_size 10000000; is decimal. Sending raw bodies:
limit 10000000 (decimal) 10000000 bytes -> 200 10000001 bytes -> 413 10485760 bytes -> 413
limit 10m (binary) 1048577 bytes -> 200 10485760 bytes -> 200 10485761 bytes -> 413
The decimal-limit rows are from a location block with client_max_body_size 10000000;. The binary-limit rows are from an earlier run of the same server with client_max_body_size 10m;, where a 10,485,760-byte body returned 200 and 10,485,761 returned 413. Whatever your documentation says, measure the byte count where the rejection happens. The KB vs KiB guide has the unit table.
The request is bigger than the file
Limits at a proxy usually apply to the whole request body, not to the file inside it. A browser form upload is multipart/form-data, which wraps the file in a boundary line and part headers. We captured real requests with a small Node.js server that logs Content-Length:
10-byte file -> Content-Length 225 (215 bytes of overhead)
1,048,576-byte file -> Content-Length 1048790 (214 bytes)
10,485,760-byte file -> Content-Length 10485975 (215 bytes)
same, longer filename -> Content-Length 10486016 (256 bytes)
Those were sent with curl -F "file=@name.bin". The overhead is a few hundred bytes and depends on the boundary string, the field name and the filename. Other clients use other boundary strings, so measure the overhead of the client you care about.
The consequence: a file exactly at a proxy’s limit, sent as a form upload, is over the limit. Against the default nginx configuration (1m) we got:
raw body, 1048576 bytes -> HTTP 200
form file, 1048576 bytes -> HTTP 413 (request body 1048789 bytes)
form file, 1048575 bytes -> HTTP 413 (request body 1048788 bytes)
The error log line was client intended to send too large body: 1048789 bytes. So test both ways: the raw body finds the limit, and the form upload finds the largest file a user can really send. The “largest accepted file” is the limit minus the overhead.
APIs that take files as JSON add Base64, which turns every 3 bytes into 4 characters. We measured base64 -w0: a 1,048,576-byte file becomes 1,398,104 characters, and a 10,485,760-byte file becomes 13,981,016. That is ceil(n / 3) * 4, about a third larger, before the JSON quotes and other fields.
Run the test: a script you can copy
This script sends N-1, N and N+1 bytes as a raw POST body and prints the status code for each:
#!/bin/sh
# usage: boundary.sh URL LIMIT_BYTES
url=$1; limit=$2
for n in $((limit - 1)) "$limit" $((limit + 1)); do
head -c "$n" /dev/zero > probe.bin
code=$(curl -s -o /dev/null -w '%{http_code}' -X POST --data-binary @probe.bin "$url")
echo "$n bytes -> HTTP $code"
done
rm -f probe.bin
Run against nginx with its default 1 MiB limit (./boundary.sh http://127.0.0.1:8080/ 1048576), it printed:
1048575 bytes -> HTTP 200
1048576 bytes -> HTTP 200
1048577 bytes -> HTTP 413
Swap --data-binary @probe.bin for -F "file=@probe.bin" to test the form path. This script uses zero-filled files, which is fine for a size check. If the server checks the file type, a zero-filled file is rejected for the wrong reason, so use a real PDF, CSV or ZIP of the exact size instead. For upload paths that also validate content, read validating uploads with file signatures.
We did not test browser automation such as Playwright, so we do not show it here.
A checklist for a pass
- The lowest limit in the chain is known, with its unit, and N-1 passes at each layer.
- N+1 fails with a 413 or a readable validation message, not a 500 or a timeout.
- After a failed upload, no partial file remains in storage and the user can retry.
- The form-upload path was tested as well as the raw body, so the overhead is accounted for.
- Both the decimal and binary readings of the limit were tested.
Try it
Generate the three files at your limit with the Dummy PDF file page (for example the Dummy 10 MB PDF file page, or type any byte count), or use the Dummy CSV file, Dummy Excel file, Dummy Word file, Dummy PNG file, Dummy XML file and Dummy ZIP file pages when your endpoint only accepts those types. Everything is built in your browser and nothing is uploaded.
Sources
Accessed 30 September 2026.
- RFC 9110, section 15.5.14, 413 Content Too Large: https://www.rfc-editor.org/rfc/rfc9110#section-15.5.14
- nginx,
client_max_body_sizein ngx_http_core_module: https://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size - Cloudflare, Error 413: https://developers.cloudflare.com/support/troubleshooting/http-status-codes/4xx-client-error/error-413/
- PHP manual, core ini directives: https://www.php.net/manual/en/ini.core.php
- Byte counts: our own runs of curl 8.21.0, nginx 1.30.5, Node.js v22.13.0 and GNU
base64on Windows 11, 29 September 2026.