You select a JPEG in the WordPress media library, the progress bar gets partway across, and then the thumbnail flips to a red box reading HTTP error. No file path, no status code, no hint about what failed. Just those two words.
The message is vague on purpose. WordPress shows it whenever the JavaScript uploader in wp-admin receives any non-200 response (or an empty body) from async-upload.php, regardless of whether the failure was in PHP, the web server, or a proxy in front of them. The real cause is always in a log somewhere. This article walks through the five things that actually produce it, how to tell them apart in under ten minutes, and how to fix each one with a verification you can run before you call it done.
What the error actually means
The WordPress media uploader is a browser-side script that POSTs each file to /wp-admin/async-upload.php as a multipart request. On success, the endpoint returns JSON with an attachment ID and a thumbnail URL. On any other response, the uploader gives up and shows HTTP error in the library. That is all the visible error ever tells you.
The real status code is in the browser's network tab. Open developer tools (F12 in Chrome or Firefox), go to the Network panel, retry the upload, and click the async-upload.php request. The status code on that row is the one that matters:
- 413 Payload Too Large points at a size cap in Apache, nginx, or a proxy like Cloudflare.
- 500 Internal Server Error points at PHP itself, almost always a memory or timeout problem mid-upload.
- 502 Bad Gateway or 504 Gateway Timeout point at PHP-FPM dying or running past its own request timeout.
- 403 Forbidden points at a WAF rule (mod_security, Wordfence, Sucuri, a Cloudflare Firewall rule).
- A completed request with an empty or non-JSON body points at a PHP fatal that killed the worker after the response started.
That single number narrows the diagnosis from "five possible causes" to "one or two". Before you change any setting, get that number.
Common causes, most likely first
- The file is larger than
upload_max_filesizeorpost_max_size. The default PHP values areupload_max_filesize = 2Mandpost_max_size = 8M, and budget hosts routinely leave them at 8 or 16 MB. A modern phone photo at 12 MP is already 4 to 6 MB, and a raw JPEG straight from a camera can be 20 MB before it even reaches the server. When the file exceeds either limit, PHP refuses the upload (see how to increase the file upload size in WordPress for the fix) and the response comes back as a 413 or an empty body. - Imagick runs out of memory decoding a large image. This is the second most common cause and the hardest for site owners to spot. The file fits under the upload limit, but when WordPress asks Imagick to generate thumbnails, Imagick loads the whole image into memory uncompressed. A 6000x4000 JPEG that is 4 MB on disk can need around 200 MB of working memory to decode. If the PHP
memory_limitis lower, the upload fails as a 500 withAllowed memory size exhaustedin the log. - The upload runs past
max_execution_time,max_input_time, or the PHP-FPM request timeout. A slow connection uploading a 30 MB file can easily blow past the defaultmax_input_timeof 60 seconds, which is the timer specifically for receiving POST data. On PHP-FPM,request_terminate_timeoutin the pool config kills workers that run longer than its value, usually 30 or 60 seconds. Either one produces a 500 or a 504 and an empty response body. - Apache
LimitRequestBodyor a WAF rule blocks the request before PHP sees it. Apache'sLimitRequestBodydirective caps the HTTP body size at the web server level, before PHP gets a chance. Some Red Hat based hosts ship with a 512 KB default. Security rules inmod_security, Wordfence, Sucuri, or a Cloudflare WAF can also reject uploads that match a pattern (a filename with<script>, a MIME type flagged as risky, an unusually large multipart body). These show up as 403, 413, or the request just never reaching PHP. - Last resort: an unusual filename or file type. This is the folklore fix that worked in 2014 and is almost never the cause today. WordPress strips most special characters from filenames during
sanitize_file_name(), so apostrophes, spaces, and accents rarely break anything. If you have genuinely ruled out causes 1 through 4 and a rename is the only thing that fixes one specific file, the cause is almost always a WAF rule matching on the original filename, not WordPress itself.
Diagnose which cause applies
Before you change any limit, narrow down which one is the active ceiling. Otherwise you risk raising something that was not the problem and masking the real issue.
Check 1: read the status code in the browser's network tab. This is the single most informative step and the one almost every walkthrough skips. Open devtools, reload the upload, and read the status on the async-upload.php row. Match it against the list above. You will know you found the signal when the status code is one of 413, 500, 502, 504, or 403, with a timestamp that matches the failing upload.
Check 2: read the server error log. Open your hosting control panel and look for "Error Logs" (cPanel, Plesk, DirectAdmin, and most managed hosts all expose this). The line with the matching timestamp tells you the exact cause. If you have SSH access: the log lives at /var/log/apache2/error.log (Apache) or /var/log/nginx/error.log (nginx). You want to see one of:
PHP Fatal error: Allowed memory size of 134217728 bytes exhausted
(tried to allocate 8192 bytes) in /.../wp-admin/includes/image.php on line 412
PHP Warning: POST Content-Length of 25165824 bytes exceeds
the limit of 8388608 bytes
client intended to send too large body: 25165824 bytes
ModSecurity: Access denied with code 403
Each of those lines points at a different one of the five causes. You will know you found the right line when its timestamp matches the failing upload and its text names a concrete limit or rule.
Check 3: read your current PHP limits. Drop a tiny diagnostic file in your WordPress root to dump the four values that govern uploads:
<?php
// upload-check.php, delete after use
echo 'upload_max_filesize: ' . ini_get('upload_max_filesize') . PHP_EOL;
echo 'post_max_size: ' . ini_get('post_max_size') . PHP_EOL;
echo 'memory_limit: ' . ini_get('memory_limit') . PHP_EOL;
echo 'max_execution_time: ' . ini_get('max_execution_time') . PHP_EOL;
echo 'max_input_time: ' . ini_get('max_input_time') . PHP_EOL;
Visit https://yoursite.nl/upload-check.php and read the values. Expected output on a healthy WordPress host: upload_max_filesize: 64M, post_max_size: 64M, memory_limit: 256M, max_execution_time: 120, max_input_time: 120. If any of those are near the PHP defaults (2M, 8M, 128M, 30, 60), you have found the likely culprit. Delete the file after reading it.
Fix 1: raise upload_max_filesize and post_max_size
This is the fix for cause 1 (413s, "POST Content-Length ... exceeds the limit", files over 8 MB that bounce off the default). The two settings must move together, and post_max_size must stay at least as large as upload_max_filesize or uploads break the other way.
- Log into your host control panel and open MultiPHP INI Editor (cPanel), PHP Settings (Plesk), PHP Selector (DirectAdmin), or PHP Configuration (hPanel).
- Select the domain whose WordPress site is failing.
- Set
upload_max_filesizeto64Mandpost_max_sizeto64M. Do not jump to512M. A reasonable cap still catches genuinely broken uploads and keeps the request bounded. - Save. Most panels apply the change to the next request with no restart.
If your host does not expose those knobs, add them to a .user.ini in your WordPress root (same folder as wp-config.php):
upload_max_filesize = 64M
post_max_size = 64M
PHP-FPM rereads .user.ini files every 300 seconds by default, so wait up to five minutes before testing.
Verify: reload the diagnostic file. You will know it worked when both values now read 64M. Then retry the upload. You will know the fix held when the file lands in the media library and the JSON response in the network tab is a 200 with an attachment ID.
Fix 2: raise memory_limit for image processing
This is the fix for cause 2, where the file fits but thumbnail generation runs out of memory. The error log line will read Allowed memory size ... exhausted and point at a file inside wp-admin/includes/image.php or wp-includes/class-wp-image-editor-imagick.php.
Raising the PHP memory_limit is the canonical fix and the full procedure lives in the dedicated article on Allowed memory size exhausted. For the upload case specifically: set memory_limit to 256M in your host panel, or add memory_limit = 256M to .user.ini.
If raising memory to 256 MB does not help and you are on shared hosting where Imagick is the default editor, you can force WordPress to use GD instead. GD is less feature-rich but uses less working memory. The correct pattern, verified against the wp_image_editors filter documentation, is a tiny mu-plugin at wp-content/mu-plugins/prefer-gd.php:
<?php
// Prefer GD over Imagick for thumbnail generation.
add_filter( 'wp_image_editors', function( $editors ) {
return array( 'WP_Image_Editor_GD', 'WP_Image_Editor_Imagick' );
} );
The filter receives an array of editor class names in priority order. Returning WP_Image_Editor_GD first makes WordPress try GD before Imagick. Putting the snippet in mu-plugins means a theme update cannot overwrite it.
Verify: retry the upload that was failing. You will know the fix held when the image lands in the library and the error log no longer contains new Allowed memory size lines for image files. Check with Query Monitor if you want to confirm which editor handled the request.
Fix 3: raise execution and input timeouts
This is the fix for cause 3, where a large upload runs past the clock. The symptom is a 500 or 504 on the async-upload.php row and an error log line mentioning Maximum execution time or POST Content-Length, or no PHP error at all because PHP-FPM killed the worker first.
Raise both max_execution_time and max_input_time in the host panel or .user.ini:
max_execution_time = 300
max_input_time = 300
max_input_time is the one that most frequently gets missed. It is the timer for receiving POST data, separate from the script's own wall clock, and its default is 60 seconds. The full procedure for raising PHP execution limits is in the article on Maximum execution time exceeded.
On PHP-FPM you may also need to raise request_terminate_timeout in the pool config (typically /etc/php/8.3/fpm/pool.d/www.conf). That setting is a hard kill above PHP's own timers, and on managed hosting only the host can change it.
Verify: reload the diagnostic file. You will know the PHP-level change stuck when max_execution_time and max_input_time both read 300. Retry the upload. You will know the fix held when a large file that previously died at 30 or 60 seconds now completes, and async-upload.php returns a 200 in the network tab.
Fix 4: check Apache LimitRequestBody and WAF rules
This is the fix for cause 4, where the web server or a WAF blocks the request before PHP ever runs. The tell is that your PHP limits are all generous, the PHP error log is silent, but the upload still returns 413 or 403 with no matching PHP entry.
For Apache, check LimitRequestBody in the virtual host config or .htaccess. The directive defaults to 0 (unlimited), but some hosts override it. On cPanel hosts with EasyApache, the value is set in the Apache configuration editor. Raise it to at least 67108864 (64 MB) to match your post_max_size:
LimitRequestBody 67108864
For nginx, the equivalent is client_max_body_size in the server or location block, usually in /etc/nginx/nginx.conf or a site-specific file. Set it to 64m:
client_max_body_size 64m;
Neither of these is typically editable on shared hosting. If the host panel does not expose them, open a support ticket with the exact error log line.
For WAF rules, the diagnosis is targeted deactivation. If the site uses Wordfence, temporarily set the firewall to "Learning Mode" and retry. If it uses Cloudflare, check the Security Events log for a block on the upload URL. If the site is on a host with mod_security and you see ModSecurity: Access denied in the log, the host has to whitelist the specific rule for /wp-admin/async-upload.php.
Verify: retry the upload. You will know the fix held when the async-upload.php row returns a 200, the server error log has no new 413 or mod_security lines, and a subsequent large upload completes without incident.
Fix 5 (last resort): rename the file
Renaming the file is the folklore fix that still circulates in old forum threads. It is almost never the actual cause in 2026, because sanitize_file_name() strips most problematic characters before the upload hits disk. The only scenario where a rename genuinely helps is when a WAF rule is matching on the original filename, which means cause 4 is the real issue and the rename is just working around it.
If you have tried fixes 1 through 4 and one specific file still fails while others succeed, rename it to something simple like test-image.jpg and retry. You will know the rename was a workaround for a WAF when the rename works but the original filename fails consistently even after you confirm the PHP and web server limits are generous. In that case the correct fix is fix 4 (whitelist the file or the rule), not a permanent rename policy.
When to escalate
If you have worked through fixes 1 through 4 and the upload still errors, collect the following before opening a support ticket or calling in a developer. The first thing anyone experienced will ask for is exactly this list:
- The exact file that fails, including its size in MB and its dimensions in pixels.
- The status code from the browser's network tab on
async-upload.php(413, 500, 502, 503, 504, 403, or 200 with empty body). - The matching line from the server error log (check "Error Logs" in your hosting panel, or
/var/log/apache2/error.log//var/log/nginx/error.logif you have SSH access), including the timestamp. - The five values from the diagnostic file in Check 3:
upload_max_filesize,post_max_size,memory_limit,max_execution_time,max_input_time. - Your WordPress version, PHP version, and SAPI (visible under Tools: Site Health: Info: Server).
- Your hosting plan name and whether you have access to
.user.ini,.htaccess, or the PHP-FPM pool config. - Whether the site is behind Cloudflare or another proxy, and whether the error also happens when you bypass it.
- The list of active security plugins (Wordfence, Sucuri, iThemes Security, All-In-One Security).
Send those in the first message. It routes the ticket to the right engineer without a round trip and prevents the host from asking you to reproduce the failure again.
If the same upload produces a bare 500 Internal Server Error on the front-end of the site while the admin upload is failing, the two are almost certainly linked: a PHP fatal during image processing can take down a request anywhere it is invoked, not just in the media library. Treat them as the same incident and follow the 500 article for the diagnosis path there.
How to prevent it from coming back
A site that keeps hitting this error is telling you its hosting ceiling is too close to its typical workload. Three habits push the margin back:
- Size images before you upload them. A hero image does not need to be a 20 MP raw. Resize phone photos to 2000 pixels on the long edge before adding them, and compress JPEGs with a tool like Squoosh. This cuts both the upload size and the memory Imagick needs to process it.
- Raise PHP limits once, not per incident. Set
upload_max_filesize,post_max_size,memory_limit, andmax_input_timeto values that cover your real-world uploads with headroom, and leave them there. Editing limits in a panic during an incident is how mistakes happen. - Match the hosting plan to the media volume. If your site regularly uploads large images and the budget host keeps hitting limits, upgrading is cheaper than a Saturday night debugging session. Modern WordPress hosting ships with sensible defaults that prevent most of these errors by construction.