An upload that works on your laptop and fails on the server is almost always a size limit, and there are at least six of them, spread across PHP, the web server and sometimes a proxy in front of both. Here is what each one does, how they interact, and how to find which is biting.
The PHP settings
| Directive | Default | What it limits |
|---|---|---|
upload_max_filesize | 2M | The size of any single uploaded file. |
post_max_size | 8M | The size of the entire POST body: all files plus all form fields. |
max_file_uploads | 20 | How many files one request may carry. |
memory_limit | 128M | Memory per script. Matters if you read files into memory. |
max_execution_time | 30 | Seconds of script execution. Excludes time spent receiving the body. |
max_input_time | -1 | Seconds allowed to parse the request, including receiving the upload. -1 means use max_execution_time. |
The ordering rule that catches everyone: post_max_size must be larger than upload_max_filesize, because the file travels inside the POST body along with the multipart boundaries and any other fields. Setting upload_max_filesize = 50M while post_max_size stays at 8M gets you a hard failure at 8 MB, and the symptom is confusing: not an upload error, but an empty $_FILES and $_POST.
For a 50 MB file, something like this is sane:
upload_max_filesize = 50M
post_max_size = 56M ; headroom for fields and multipart overhead
max_file_uploads = 40
max_input_time = 300 ; slow connections need time to send
memory_limit = 128M ; no need to raise if you stream, see below
Why exceeding post_max_size is so confusing
When the body exceeds post_max_size, PHP discards it before your script runs. There is no UPLOAD_ERR_* entry to check, because there is no entry at all — $_FILES is an empty array. Detect it explicitly:
if ($_SERVER['REQUEST_METHOD'] === 'POST'
&& empty($_POST) && empty($_FILES)
&& ($_SERVER['CONTENT_LENGTH'] ?? 0) > 0) {
$max = ini_get('post_max_size');
http_response_code(413);
exit("Upload exceeded the server limit of {$max}.");
}
Without that check the user sees a blank page or a "no file selected" message, which sends everyone hunting in the wrong place.
The limits above PHP
Even with PHP configured generously, the web server can refuse the request before PHP is reached. These are the ones people forget:
Nginx
client_max_body_size 60m; # default 1m - returns 413 well before PHP sees it
client_body_timeout 300s;
The default of 1 MB is far smaller than PHP's own, so on an Nginx box it is usually the first wall you hit. The error is a plain 413 Request Entity Too Large from Nginx, with nothing in the PHP log.
Apache
LimitRequestBody 62914560 # bytes; 0 = unlimited (the default on most builds)
If PHP runs through mod_php you can set the PHP values in .htaccess too:
php_value upload_max_filesize 50M
php_value post_max_size 56M
That only works for mod_php. Under PHP-FPM or CGI those lines are ignored (and with AllowOverride restricted they can produce a 500), so use .user.ini or the pool configuration instead.
IIS
IIS applies request filtering before the PHP handler:
<system.webServer>
<security><requestFiltering>
<!-- bytes; default is 30000000, about 28.6 MB -->
<requestLimits maxAllowedContentLength="62914560" />
</requestFiltering></security>
</system.webServer>
Exceeding it produces a 404.13, which looks like a missing file rather than a size problem — another reason large-upload debugging takes longer than it should.
Proxies and CDNs
A reverse proxy or CDN in front of the origin has its own body cap, and it is often the real limit on a modern stack. Check that before rewriting any PHP.
Finding which limit is actually biting
Print what PHP believes, on the server in question:
<?php
foreach ([
'upload_max_filesize', 'post_max_size', 'max_file_uploads',
'memory_limit', 'max_execution_time', 'max_input_time', 'upload_tmp_dir',
] as $k) {
printf("%-22s %s\n", $k, var_export(ini_get($k), true));
}
echo 'Loaded php.ini: ' . php_ini_loaded_file() . "\n";
That last line matters more than it looks: editing the wrong php.ini is the most common reason a change "does nothing". Under PHP-FPM, remember to reload the service — the pool caches configuration.
Then match the symptom:
- 413 with no PHP log entry — Nginx
client_max_body_size, ApacheLimitRequestBody, or a proxy. - 404.13 on IIS —
maxAllowedContentLength. - Empty
$_FILESand$_POST—post_max_size. errorisUPLOAD_ERR_INI_SIZE(1) —upload_max_filesize.errorisUPLOAD_ERR_FORM_SIZE(2) — theMAX_FILE_SIZEhidden field in the form, not a server setting.- Files beyond the twentieth vanish —
max_file_uploads. - Fails only on slow connections —
max_input_time.
Does memory_limit need raising?
Usually not. PHP writes the incoming upload to a temporary file on disk (upload_tmp_dir), so a 500 MB upload does not need 500 MB of memory. Memory becomes a problem only when your code loads the file — file_get_contents() on the temp file, an image library that decodes the whole bitmap, or base64-encoding it for storage. Stream instead:
$in = fopen($file['tmp_name'], 'rb');
$out = fopen($target, 'wb');
stream_copy_to_stream($in, $out); // constant memory regardless of size
fclose($in);
fclose($out);
Raising limits is not the whole answer
You can push these numbers up, but a single 2 GB POST remains fragile: one dropped connection and the user starts over, with no progress indication while they wait. Past a few hundred megabytes the better answer is to stop sending one enormous request and split the upload into chunks.
That is the approach PHP File Uploader takes: files are transferred in parts and streamed to disk, so upload_max_filesize and post_max_size stop being the binding constraint, and an interrupted transfer can resume instead of restarting. The large-file demo shows it, and the installation guide covers the server-side setup.
