ExactSize

"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-stream with 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-Length by 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 PNG83,101110,804110,89983,2951.3345
2000 × 2000 JPEG117,632156,844156,944117,8301.3342
4000 × 6000 PNG scan1,297,0681,729,4241,729,5201,297,2631.3334
4000 × 6000 JPEG photo3,492,6784,656,9044,657,0033,492,8751.3334
6000 × 9000 JPEG photo11,525,36515,367,15615,367,25511,525,5621.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,6781.0000—
base64 of that file4,656,9041.33331.3333
gzip of the base643,470,7450.99370.7453
gzip of the raw file, for comparison3,460,1290.99070.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

  1. Open DevTools (F12), go to Network, and filter to Fetch/XHR.
  2. Start the upload. A new request appears — usually a POST, often named upload, attach or a bare endpoint path.
  3. Click it and read Headers → Request Headers → Content-Type. application/json means base64 (or some other text encoding); multipart/form-data; boundary=… means the file is travelling as raw bytes.
  4. 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.
  5. If the payload is too large for DevTools to preview, do the arithmetic instead: divide Content-Length by 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 fits7,499,9349,999,9129,999,997−3
One byte more7,499,9359,999,91610,000,001+1
10 MB × 0.75 exactly7,500,00010,000,00010,000,085+85
Compressor output at a 7,324 KB target7,341,7099,788,9489,789,033−210,967
Compressor output at a 9,765 KB target9,841,88613,122,51613,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,0007,499,9347,324 KB9,999,8029,765 KB
8,388,608 (PHP's 8M)6,291,3906,143 KB8,388,4108,191 KB
5,000,0003,749,9343,662 KB4,999,8024,882 KB
4,000,0002,999,9342,929 KB3,999,8023,906 KB
2,000,0001,499,9341,464 KB1,999,8021,952 KB
1,048,576786,366767 KB1,048,3781,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 KB7,341,70990.16%9,789,03311,968 ms
multipart9,765 KB9,841,88695.31%9,842,06921,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:

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.