"File too large to upload" — the server never receives the file you have
If the form says 10 MB and the page posts your picture as base64 inside JSON, compress to 7,324 KB. Not 10,000 KB, and not 7,500 KB either. Base64 turns every 3 bytes into 4, so the body is 4/3 the file, and the JSON wrapper around it adds another 85 bytes: 7,500,000 bytes of file becomes a 10,000,085-byte body and is rejected by 85 bytes. The measured ceiling is 7,499,934 bytes.
If the page posts multipart/form-data instead, 9,765 KB is enough — the
wrapper there measures 183 to 198 bytes, and going down to 7,324 KB would cost 5.2 points of quality for
nothing. Same picture, same form, same limit; the coefficient depends on one header you can read in ten
seconds.
Which coefficient applies to you
- Body starts
{"file":"data:image/jpeg;base64,— a base64 uploader. Multiply by 1.3333 and subtract the wrapper: file budget = (limit − 85) × 0.75, where 85 is the envelope for a short filename (95–100 bytes for the longer names measured here). For 10 MB, type 7,324 KB. Content-Type: multipart/form-data; boundary=…— the body is the file plus 183–198 bytes. For 10 MB, type 9,765 KB. Do not use 0.75 here.application/octet-streamwith the raw file as the body — coefficient 1.0000. The file budget is the limit minus a few bytes of headers.- No DevTools, no idea: divide the request's
Content-Lengthby the file's byte count. ≈1.33 is base64, ≈1.00 is multipart. Measured values below are 1.3333 and 1.00002.
Every number on this page comes from a body that was built and posted on 26 October 2026 and counted byte by byte at the receiving end. Five real files, three real compressor outputs, and one arithmetic step that the text always labels as arithmetic.
Test conditions: Chrome 155 on Windows 11. Each file was read into a Blob,
converted with FileReader.readAsDataURL, wrapped in the two envelopes a real uploader uses,
and posted to a local HTTP server that counted the bytes as they arrived. The JSON envelope was
{"filename":…,"content_type":…,"file":"data:image/jpeg;base64,…"}; the multipart body was
generated by the browser's own FormData with its own WebKit boundary. Source files:
83,101 B PNG, 117,632 B JPEG, 1,297,068 B PNG, 3,492,678 B JPEG, 11,525,365 B JPEG. One sample per row,
not a distribution.
One file, one 10 MB limit, two answers 3.1 MB apart
This is the case that produces the search. A 6000 × 9000 photograph compressed to
9,841,886 bytes — 9.39 MB, comfortably inside a 10 MB limit, and inside it by either
reading of "MB". Posted as multipart/form-data, the body arrived at
9,842,069 bytes and passed. The identical bytes posted as base64 inside JSON arrived at
13,122,601 bytes and were over by 3,122,601.
Nothing about the picture changed between those two requests. Same pixels, same quality setting, same file. The only difference is how the page chose to encode it, and that choice is made by whoever wrote the upload form, not by you. Which is why "my file is under the limit and it still fails" is not a contradiction — it is two different numbers wearing the same label.
Three numbers per file, and the middle one is not a guess
Base64 is often described with a formula, so here are both: the formula's result and the length the browser actually produced, side by side with what a server actually received.
| File | On disk | base64 length | JSON body received | multipart body received | Body ÷ disk |
|---|---|---|---|---|---|
| 1920 × 1080 PNG | 83,101 | 110,804 | 110,899 | 83,295 | 1.3345 |
| 2000 × 2000 JPEG | 117,632 | 156,844 | 156,944 | 117,830 | 1.3342 |
| 4000 × 6000 PNG scan | 1,297,068 | 1,729,424 | 1,729,520 | 1,297,263 | 1.3334 |
| 4000 × 6000 JPEG photo | 3,492,678 | 4,656,904 | 4,657,003 | 3,492,875 | 1.3334 |
| 6000 × 9000 JPEG photo | 11,525,365 | 15,367,156 | 15,367,255 | 11,525,562 | 1.3333 |
All values in bytes. "base64 length" is the browser's data URL length minus the 23-byte
data:image/jpeg;base64, prefix, and it matched 4 × ⌈n/3⌉ exactly on all five files.
"Received" is the byte count the local server accumulated from the request stream, not a computed
value. The JSON envelope added 95–100 bytes for these filenames and 85 for photo.jpg; the
multipart envelope added 194, 198, 195, 197 and 197 bytes respectively.
Two things in that table are worth reading twice. The first is the ratio column: it is 1.3333 to 1.3345, and the drift is entirely explained by the fixed-size wrapper landing on files of different sizes — the small file gets a slightly higher ratio because 95 bytes is a bigger fraction of 83,101 than of 11,525,365. The second is how thin the multipart envelope is. 194 bytes of boundary and headers, on a file of 83 KB. That is the whole difference between a form that accepts a 9.6 MB file and one that rejects it.
The 3,492,678-byte photo is a useful middle case because it sits between two common limits. Its base64 body is 4,657,003 bytes, so a form with a 4 MB ceiling rejects it by 657,003 bytes — while its multipart body, 3,492,875 bytes, passes the same form untouched. If you have ever compressed a 3.3 MB photo to fit a 4 MB form and then wondered why the untouched original worked on a different site, this is the mechanism.
Why it is 4/3, and why gzip hands most of it back
Base64 exists to carry binary data through channels that only accept text. It takes 3 input bytes — 24 bits — and cuts them into four 6-bit groups, each mapped to one of 64 printable characters. Four characters where three bytes used to be. 4/3 = 1.3333, and there is no way around it: the encoding is lossless, so the information has to fit in a character set with fewer states, and the price is 33.3% more symbols.
That also means the overhead is compressible in a way a JPEG is not. A JPEG is already entropy-coded, so gzip barely touches it; base64 is 6 bits of payload per 8-bit character, so a general-purpose compressor can recover almost exactly the quarter that was added. Measured on the 3,492,678-byte photo:
| Stage | Bytes | vs file | vs previous |
|---|---|---|---|
| File on disk (JPEG) | 3,492,678 | 1.0000 | — |
| base64 of that file | 4,656,904 | 1.3333 | 1.3333 |
| gzip of the base64 | 3,470,745 | 0.9937 | 0.7453 |
| gzip of the raw file, for comparison | 3,460,129 | 0.9907 | 0.9907 |
gzip level 9, one file, measured with Node's zlib on 26 October 2026. The gzipped base64
is 3,470,745 bytes against 3,460,129 for the gzipped raw file — 10,616 bytes larger, or 0.31%, which is
what is left of the 33.3% after compression.
So if the uploader sets Content-Encoding: gzip, the 33.3% tax mostly disappears on the wire.
Whether that helps you depends on where the limit is enforced, and this is the part worth being precise
about: a limit checked on the request body before decompression is dodged by gzip, and a limit checked on
the decoded content is not. We did not test a live endpoint to see which one a given site does, so treat
this paragraph as a reason to look at the request headers rather than as a fix.
Finding out which one you are up against, in ten seconds
- Open DevTools (F12), go to Network, and filter to Fetch/XHR.
- Start the upload. A new request appears — usually a POST, often named
upload,attachor a bare endpoint path. - Click it and read Headers → Request Headers → Content-Type.
application/jsonmeans base64 (or some other text encoding);multipart/form-data; boundary=…means the file is travelling as raw bytes. - Confirm in the Payload tab. A multipart request shows a "Form Data" table with your
filename in it. A base64 request shows a "Request Payload" of JSON with a string that begins
data:image/jpeg;base64,and runs for megabytes. - If the payload is too large for DevTools to preview, do the arithmetic instead: divide
Content-Lengthby the file's byte count. 1.33 is base64, 1.00 is multipart.
One caution on that last step: Content-Length can be absent when the browser streams the
body with Transfer-Encoding: chunked. In the eight requests measured here it was always
present, but a missing header is not evidence of anything — fall back to the Content-Type.
The 0.75 rule, and the 85 bytes it misses
The shortcut everyone repeats is "compress to 75% of the limit". It is right about the coefficient and wrong about the boundary, because 75% of the limit is exactly where the body lands, and then the wrapper pushes it over. Measured, on the same envelope, against a 10,000,000-byte ceiling:
| What was sent | File bytes | base64 | Body received | vs 10,000,000 |
|---|---|---|---|---|
| Largest file that fits | 7,499,934 | 9,999,912 | 9,999,997 | −3 |
| One byte more | 7,499,935 | 9,999,916 | 10,000,001 | +1 |
| 10 MB × 0.75 exactly | 7,500,000 | 10,000,000 | 10,000,085 | +85 |
| Compressor output at a 7,324 KB target | 7,341,709 | 9,788,948 | 9,789,033 | −210,967 |
| Compressor output at a 9,765 KB target | 9,841,886 | 13,122,516 | 13,122,601 | +3,122,601 |
Every row was posted and counted at the receiving end. The first three rows use the real
6000 × 9000 JPEG trimmed to an exact byte length so the body lands either side of the ceiling — the
payload is a truncated copy of that file, and the byte arithmetic is the point of the row, not the
picture. Envelope: 85 bytes, the same photo.jpg wrapper as above.
Three conclusions, and the third one is the reason this table exists.
The boundary is one byte wide. 7,499,934 bytes arrives as 9,999,997 and passes by 3
bytes. 7,499,935 bytes arrives as 10,000,001 and fails by 1. There is no rounding and no tolerance
anywhere in this path; a server comparing Content-Length to a limit does exact integer
arithmetic.
The 0.75 shortcut lands on the wrong side of it. 7,500,000 is exactly 75% of 10,000,000, and its base64 is exactly 10,000,000 — the ceiling, not under it. Add the 85-byte envelope and the body is 10,000,085. The shortcut is off by 85 bytes and the file is rejected, which is the least satisfying kind of failure: the arithmetic looked right.
And the practical setting has more slack than the ceiling suggests. A 7,324 KB target produced 7,341,709 bytes on the 54-megapixel source, which arrives as 9,789,033 — 210,967 bytes under. That gap is not waste, it is the compressor's quality search landing on a step rather than a continuum. Which is also why a 7,400 KB target produced the identical 7,341,709 bytes at the identical quality: both targets converged on the same encode, 76 KB of budget apart. Do not chase the last 76 KB.
What to type, for each limit you are likely to meet
| Stated limit (bytes) | base64: max file | base64: type this | multipart: max file | multipart: type this |
|---|---|---|---|---|
| 10,000,000 | 7,499,934 | 7,324 KB | 9,999,802 | 9,765 KB |
| 8,388,608 (PHP's 8M) | 6,291,390 | 6,143 KB | 8,388,410 | 8,191 KB |
| 5,000,000 | 3,749,934 | 3,662 KB | 4,999,802 | 4,882 KB |
| 4,000,000 | 2,999,934 | 2,929 KB | 3,999,802 | 3,906 KB |
| 2,000,000 | 1,499,934 | 1,464 KB | 1,999,802 | 1,952 KB |
| 1,048,576 | 786,366 | 767 KB | 1,048,378 | 1,023 KB |
The base64 column is (limit − 85) × 0.75, rounded down to a whole number of base64 groups — arithmetic from the measured 85-byte envelope. The multipart column is limit − 198, using the largest envelope measured for a multipart body. "Type this" is the largest whole KB that stays under that file size; this compressor counts 1,024 bytes to a kilobyte, which is why the column reads 7,324 and not 7,324.15.
The 8,388,608 row is not hypothetical. It is PHP's default post_max_size, and the manual
describes exactly the failure this page is about:
From php.net, core php.ini directives, post_max_size: "Sets max size of post data allowed. This setting also affects file upload. To upload large files, this value must be larger than upload_max_filesize. … If the size of post data is greater than post_max_size, the $_POST and $_FILES superglobals are empty." Documented default: "8M". Read on 26 October 2026.
Read that last sentence again, because it explains the vagueness of the error. The application does not
receive a file that is too big — it receives no file at all. $_FILES is empty, so
whatever check the developer wrote ("if no file was uploaded, show an error") fires, and the message it
produces has nothing to do with the real cause. Meanwhile upload_max_filesize, the other
limit in that paragraph, defaults to "2M" and counts the uploaded file itself rather
than the request body. Two limits, two different quantities, both reported to you as one sentence.
If you are not sure which of them you are hitting, the triage is on the page about that error message, which separates request-body limits from pixel limits and format whitelists. This page is only about the first one, and about the factor that makes the first one smaller than it looks.
What the base64 setting costs on a multipart form
Take the 54-megapixel source and give it a 10,000,000-byte ceiling twice — once as base64, once as multipart. Both runs kept every pixel, 6000 × 9000.
| Uploader | Target set | Output bytes | Quality | Body received | Encode time |
|---|---|---|---|---|---|
| base64 (JSON) | 7,324 KB | 7,341,709 | 90.16% | 9,789,033 | 11,968 ms |
| multipart | 9,765 KB | 9,841,886 | 95.31% | 9,842,069 | 21,809 ms |
Both from the same 11,525,365-byte, 6000 × 9000 JPEG, JPG output, dimensions preserved in both. Times are the compressor's own "Done in … ms" reading. Note the direction: the smaller output also finished in about half the time.
The base64 tax on this picture is 5.16 quality points — 95.31% down to 90.16% — on top of 9.8 seconds less encoding work. That is the real cost, and it is the reason the two coefficients have to be told apart before you compress. Apply the base64 setting to a multipart form and you spend 5.16 points of quality to solve a problem you did not have. Apply the multipart setting to a base64 form and the upload fails at 13.1 MB.
There is a third case worth naming: the file that already fits. A 9,841,886-byte file is under 10,000,000, so on a multipart form the right move is to send it unchanged — 9,842,069 bytes of body, nothing re-encoded. Compressing it "to be safe" is not free, and the pages that measure what a budget does to a picture are the place to see the price: 500 KB holds 8.41 megapixels, 300 KB holds 4.84, and 1 MB keeps 19.75 megapixels whole. If your file is already inside the limit, none of those budgets applies to you.
When the coefficient is not 1.3333
The 0.75 factor is a property of one encoding, not a law of uploads. Four boundaries worth knowing:
- The limit is enforced on the decoded file, not the body. Then the file budget is the
limit itself, and the 1.3333 is irrelevant to the check — it only makes the transfer slower. PHP's
upload_max_filesizeis this shape;post_max_sizeis the other shape. You cannot tell which one a site uses from the outside, which is why the table above gives both columns. - The uploader sends raw binary.
application/octet-streamwith the file as the body: coefficient 1.0000, overhead is the headers only. Compressing to 75% here wastes a quarter of the budget. - The uploader chunks the file. Each chunk gets its own base64 encoding and its own envelope, so the overhead is repeated per chunk rather than paid once. This one is arithmetic rather than measurement — we did not build a chunked uploader — but the direction is unambiguous: more chunks, more wrapper.
- The request is gzipped. As measured above, gzip takes the base64 body back to 99.37% of the file's own size, so the tax disappears for anything counted after decompression.
And one boundary that applies to this site specifically: nothing here is uploaded at all. The file is read through the browser's File API, drawn onto a canvas and re-encoded inside the page — there is no request body, so the 13,122,601-byte body in this article never exists when you compress here. That is not a privacy claim, it is the reason the largest test file on this page could be measured at all: an 11.5 MB source would be rejected by most upload forms before it could be made smaller.
Questions this page gets
My file is 9.6 MB and the form says 10 MB. Why is it rejected?
Because 10 MB is probably the body limit and your uploader base64s the file. 9,841,886 bytes of file becomes 13,122,601 bytes of body — 31% over the ceiling. Compress to 7,324 KB, or check the Content-Type first and use 9,765 KB if it turns out to be multipart.
I compressed to 7.5 MB and it still fails.
7,500,000 bytes is exactly 75% of 10,000,000, and its base64 is exactly 10,000,000 — at the limit, not under it. The envelope adds 85 bytes and the body measures 10,000,085. The measured maximum is 7,499,934 bytes; type 7,324 KB and stop there.
Does this apply to phone photos too?
It applies to any file, because it is about the encoding rather than the camera. A 3,492,678-byte phone photo has a base64 body of 4,657,003 bytes, so it fails a 4 MB form that its multipart body of 3,492,875 bytes would pass.
Why does the error say nothing about size?
Because in the most common server stack it never reaches the code that would produce a good message.
nginx returns a bare 413 that browsers render poorly, and PHP returns empty $_POST and
$_FILES arrays. Both behaviours are in the official documentation, quoted on
the triage page and above.
Is 1.3333 the same as "a third bigger"?
Yes — a third of the file size is added, which is the same as multiplying by 4/3. The two phrasings differ in the other direction: a base64 body is 33.3% larger than the file, and the file is 25% smaller than the body. Getting that backwards is how a 7.5 MB target ends up at the ceiling.
Can I avoid the overhead instead of compressing for it?
Only by changing what the page sends, which you cannot do from the outside. What you can do is find out which encoding it uses — Content-Type, or Content-Length divided by file size — and set one target instead of guessing between two.
If the limit turns out to be bytes and you want the largest picture that fits, the budgets are measured per target: 1 MB keeps 19.75 megapixels, 500 KB holds 8.41, 300 KB holds 4.84. Each of those pages also lists the byte value to type for a strict decimal limit.
And if the number came from an official form rather than a web developer's default — passport photos, visa portals, civil-service applications — the published rules are read line by line on the portal requirements page, with the source URL and the date each was read.