Contents
PHP file upload limits explained
Hey, fellow developers. Picture this: it's 2 AM, your client's screaming about a broken file upload form, and you're staring at that infuriating error—"The uploaded file exceeds the upload_max_filesize directive in php.ini." We've all been there. That moment when a simple image or PDF refuses to budge because PHP's default guardrails kick in. I remember debugging one such beast on a legacy Laravel app last year—hours lost to invisible limits that felt like they were mocking me.
PHP file uploads aren't just a checkbox feature; they're a minefield of server configs, security traps, and quiet frustrations. Today, we're unpacking it all: the defaults that trip you up, how to check and tweak them, and real code to handle uploads without the headaches. Whether you're building a media gallery, an admin dashboard, or just fixing that WordPress plugin install, this'll save you time. And sanity.
The silent killers: Default PHP upload limits
Out of the box, PHP clamps down hard on uploads. Why? Security and resource protection—servers aren't infinite. But those defaults? They're tiny.
- upload_max_filesize: Caps a single file at 2MB. That's fine for a profile pic, laughable for a brochure PDF.
- post_max_size: Limits the entire POST request, often 8MB. Bigger than upload_max_filesize, but still restrictive if you're bundling form data.
- max_file_uploads: Allows 20 files max per request. Hit that with a batch upload? Poof—later files vanish.
- memory_limit: Sneaky one. Defaults vary, but if it's tight (say, 128MB), big files crash your script.
These live in php.ini, but hosts override them. Shared cPanel setups? Often 32MB tops. And don't get me started on MAX_FILE_SIZE in forms—it's a client-side hint PHP checks, but cap it at 2GB or you hit integer overflow errors pre-PHP 7.1. Browsers ignore it anyway; malicious users spoof it easy.
Ever wondered why your 5MB video stalls? post_max_size must exceed upload_max_filesize, or the whole request dies. Simple rule: always set post_max_size 20-30% higher.
Checking your limits: The first diagnostic step
Before you touch a config, know your battlefield. Drop this script in a file—call it php-limits.php—and hit it in your browser.
<?php
echo 'upload_max_filesize: ' . ini_get('upload_max_filesize') . '<br>';
echo 'post_max_size: ' . ini_get('post_max_size') . '<br>';
echo 'max_file_uploads: ' . ini_get('max_file_uploads') . '<br>';
echo 'memory_limit: ' . ini_get('memory_limit') . '<br>';
echo 'max_execution_time: ' . ini_get('max_execution_time') . '<br>';
?>
Run it. Boom—your truths revealed. I do this on every new server. Caught a DreamHost rig stuck at 2MB once; client thought it was "their file too big." Nope. Config.
Pro tip: Wrap in try-catch for edge cases, log errors. Production-ready.
Cracking the configs: Where and how to increase limits
You've checked. Limits suck. Time to lift them. Methods stack by access level—pick yours.
Method 1: php.ini (Server admins, VPS gods)
Edit your main php.ini (php –ini shows path). Add or tweak:
upload_max_filesize = 20M
post_max_size = 25M
memory_limit = 256M
max_execution_time = 300
max_file_uploads = 50
max_input_time = 300
Restart PHP-FPM or Apache. Test. Done. For 512MB beasts? Scale post_max_size to 600M+. But watch memory—uploads eat RAM.
If mod_php runs, drop in your root .htaccess:
<IfModule mod_php.c>
php_value upload_max_filesize 20M
php_value post_max_size 25M
php_value memory_limit 256M
php_value max_execution_time 300
</IfModule>
Quick, per-directory. cPanel? Use MultiPHP INI Editor: pick domain, bump upload_max_filesize, apply. Godaddy, Dreamhost—same drill via panels.
Method 3: User.ini or phprc (CGI/suexec hosts)
For suPHP or FPM per-user:
php_admin_value[upload_max_filesize] = 20M
php_admin_value[post_max_size] = 25M
Kill PHP processes post-edit (killall php-fpm). Virtualmin? Server templates.
Gotchas across the board
- Nginx/Apache proxies: Bump client_max_body_size (Nginx) or LimitRequestBody (Apache).
- IIS: web.config with
for 300MB. - 32-bit limits: Pre-64-bit PHP? 2GB cap on sizes.
- Restart everything. Limits cache.
I once chased a "partial upload" for hours—turned out max_input_time choked at 60s. Bumped to 500. Peace.
Secure uploads: Beyond size tweaks
Size is table stakes. Real pros validate. Here's a battle-tested class I refined from client wars. Handles MIME, extensions, hashing, errors. Drop it in, use via JSON endpoint.
<?php
declare(strict_types=1);
class FileUploadHandler {
private string $uploadDirectory;
private array $allowedTypes;
private array $allowedExtensions;
private int $maxFileSize;
public function __construct(
string $uploadDirectory = 'uploads/',
array $allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'],
array $allowedExtensions = ['jpg', 'jpeg', 'png', 'pdf'],
int $maxFileSize = 5242880 // 5MB
) {
$this->uploadDirectory = $uploadDirectory;
$this->allowedTypes = $allowedTypes;
$this->allowedExtensions = $allowedExtensions;
$this->maxFileSize = $maxFileSize;
$this->initializeUploadDirectory();
}
private function initializeUploadDirectory(): void {
if (!is_dir($this->uploadDirectory)) {
mkdir($this->uploadDirectory, 0755, true);
}
$htaccess = $this->uploadDirectory . '.htaccess';
if (!file_exists($htaccess)) {
file_put_contents($htaccess, "php_flag engine off\nOptions -Indexes\n");
}
}
public function handleUpload(): array {
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_FILES['file'])) {
throw new RuntimeException('Invalid upload');
}
$file = $_FILES['file'];
$this->validateUpload($file);
$fileInfo = $this->processUpload($file);
return ['success' => true, 'file' => $fileInfo];
} catch (Exception $e) {
return ['success' => false, 'message' => $e->getMessage()];
}
}
private function validateUpload(array $file): void {
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException($this->getUploadErrorMessage($file['error']));
}
if ($file['size'] > $this->maxFileSize) {
throw new RuntimeException('File too large');
}
$fileType = mime_content_type($file['tmp_name']);
if (!in_array($fileType, $this->allowedTypes, true)) {
throw new RuntimeException('Invalid type');
}
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($extension, $this->allowedExtensions, true)) {
throw new RuntimeException('Invalid extension');
}
if (!is_uploaded_file($file['tmp_name'])) {
throw new RuntimeException('Invalid upload');
}
}
private function processUpload(array $file): array {
$hash = hash_file('sha256', $file['tmp_name']);
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$newFilename = sprintf('%s_%s.%s', uniqid('', true), $hash, $extension);
$destination = $this->uploadDirectory . $newFilename;
if (!move_uploaded_file($file['tmp_name'], $destination)) {
throw new RuntimeException('Move failed');
}
chmod($destination, 0644);
return [
'name' => $file['name'],
'type' => mime_content_type($destination),
'size' => filesize($destination),
'hash' => $hash,
'path' => $destination
];
}
private function getUploadErrorMessage(int $error): string {
return match($error) {
UPLOAD_ERR_INI_SIZE => 'Exceeds upload_max_filesize',
UPLOAD_ERR_FORM_SIZE => 'Exceeds form MAX_FILE_SIZE',
UPLOAD_ERR_PARTIAL => 'Partial upload',
UPLOAD_ERR_NO_FILE => 'No file',
UPLOAD_ERR_NO_TMP_DIR => 'No temp dir',
UPLOAD_ERR_CANT_WRITE => 'Disk write fail',
UPLOAD_ERR_EXTENSION => 'Extension blocked',
default => 'Unknown error'
};
}
}
// Usage
header('Content-Type: application/json');
$handler = new FileUploadHandler(maxFileSize: 20 * 1024 * 1024); // 20MB
echo json_encode($handler->handleUpload(), JSON_THROW_ON_ERROR);
?>
Tweak constructor for your needs. Hashes dupes, secures dirs, decodes errors. Pair with frontend JS for progress bars—feels pro.
Real-world traps and quiet victories
That WordPress "exceeds directive" nag? Same fix. Drupal? Matches smaller of post/upload limits. Laravel? Filesystem disk configs layer on top.
One late-night win: Client's e-learning site choked on 50MB videos. Bumped to 100M, added chunked uploads via JS. No more timeouts. You feel it—that rush when the progress bar hits 100%.
Have you hit the multi-file wall? max_file_uploads=20 drops extras silently. Log $_FILES counts.
Security whisper: Always re-validate server-side. MIME sniffing fools no one smart.
Next time a upload fails, you'll smile. You've tamed the beast. And in that glow of the monitor, late into the night, there's a quiet nod to the code that just works—reliable, unflashy, yours.