WordPress "HTTP error" on Image Upload: A Decision Tree That Actually Finds It
Find the real cause of a WordPress HTTP error on image upload in one check, then fix the right branch: firewall, PHP memory, post_max_size, or Imagick.
Published
“HTTP error” is not a cause. It is the media uploader admitting it sent your file to the server and got back a response it could not parse as success — and at least six unrelated failures produce those identical three words. Guessing between them is why people spend an afternoon raising upload_max_filesize on a site whose real problem is a firewall rule.
The fastest route out is a single observation that splits the six causes into two groups, then one check per branch.
First: read the actual response
Open your browser’s developer tools, go to the Network tab, and upload the file again. Watch for the request to async-upload.php. Whatever comes back is your diagnosis.
| What you see | What it means | Go to |
|---|---|---|
| 403 or 406, HTML body | Firewall or mod_security rejected the request | Branch A |
| 500, or empty response, or a truncated body | PHP crashed or ran out of memory mid-process | Branch B |
| 413 | Request body exceeded a server limit | Branch C |
| 200, but body isn’t clean JSON | A plugin or theme printed output before the response | Branch D |
| Request never completes / times out | Execution time limit, usually with large files | Branch B |
That one look eliminates most of the internet’s advice for you. If you can’t get to the browser tools, turn on logging in wp-config.php first:
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
Reproduce the upload, then read wp-content/debug.log. A fatal error line names the branch outright. If the line is dense, paste it into the error log decoder — it maps the common fatal signatures to what actually broke.
Branch A — a firewall or mod_security rule
A 403 with an HTML page in the body means the request never reached WordPress. Something in front of it — mod_security, Cloudflare, Wordfence, Sucuri, or your host’s own rule set — decided the POST looked malicious.
The check: temporarily deactivate any security plugin and upload again. If that changes nothing, the rule lives at the server level, and you need your host to look at the mod_security audit log for the timestamp of the failed upload. They can whitelist the specific rule ID.
Be honest with yourself about the advice you’ll find here. Snippets telling you to drop this into .htaccess:
SecFilterEngine Off
SecFilterScanPOST Off
target mod_security 1.x, which is close to extinct. On a modern server those directives either do nothing or throw a 500 that makes the site worse. Modern mod_security uses SecRuleEngine, and most managed hosts block per-directory overrides of it anyway. Ask the host; don’t paste.
Branch B — PHP memory or execution limits
This is the most common branch and the one with the clearest tell: it’s intermittent. The same file uploads on the third try, or a 2MB photo fails while a 6MB PDF sails through. Hard configuration limits fail identically every time. Memory failures don’t, because how much is available depends on what else the server is doing.
Why images specifically: WordPress doesn’t just store the file, it decompresses it into raw pixels to generate every intermediate size — medium_large, anything registered with add_image_size, woocommerce_thumbnail if you run a store, plus a -scaled copy if the image is over the big_image_size_threshold. A 12-megapixel JPEG is a few MB on disk and roughly 48MB in memory, before the resize buffer. A 128MB limit disappears quickly.
Raise it in wp-config.php, above the “stop editing” line:
define( 'WP_MEMORY_LIMIT', '256M' );
define( 'WP_MAX_MEMORY_LIMIT', '512M' );
The real cost: WP_MEMORY_LIMIT cannot exceed what PHP itself allows. If your host caps memory_limit at 128M, this constant is decorative. Check the true value in Tools → Site Health → Info → Server, not in what you wrote. If PHP’s own limit is the ceiling, only the host can move it.
Branch C — file size and post limits
Check the actual numbers under Tools → Site Health → Info → Media Handling. Two settings matter and people only ever change one:
upload_max_filesize— the cap on a single file.post_max_size— the cap on the entire request body.
If the whole POST exceeds post_max_size, PHP discards the request before WordPress executes any code. Nothing exists to return, so the uploader gets an unparseable response and says “HTTP error”. post_max_size should always be comfortably larger than upload_max_filesize.
A caveat worth knowing: when a file exceeds only upload_max_filesize, the uploader usually catches it in the browser and says the file exceeds the maximum upload size — a clearer message. So a bare “HTTP error” points more often at post_max_size or at Branch B than at the setting everyone raises first.
Where to change them depends on your stack. On Apache with mod_php, .htaccess works:
php_value upload_max_filesize 64M
php_value post_max_size 128M
On PHP-FPM, LiteSpeed, or NGINX — which is most modern hosting — those lines do nothing, and may throw a 500. Use the host’s PHP settings panel or a .user.ini file instead. NGINX also enforces its own client_max_body_size, which only the host can raise.
Branch D — the image editor library (Imagick vs GD)
WordPress prefers ImageMagick when the PHP extension is present and falls back to GD. On shared hosts, ImageMagick is frequently the problem: its policy configuration file restricts memory, disk, and area per operation, and when a resize trips one of those limits the process dies without a useful message. You’ll usually see this on large images while small ones work — which looks like Branch B and isn’t.
Force GD to test the theory:
add_filter( 'wp_image_editors', function ( $editors ) {
return array( 'WP_Image_Editor_GD' );
} );
Put that in functions.php or a small mu-plugin, retry the upload, then remove it if nothing changes.
The honest trade-off: GD is not strictly better. It holds the full uncompressed bitmap in PHP’s memory, so for very large files it can hit memory_limit where ImageMagick — which works partly outside PHP’s allocation — would have succeeded. Switching can trade one failure for another. Treat it as a diagnostic first and a fix second.
Branch E — uploads directory permissions
Rare on managed hosting, common right after a migration or a manual server move. If wp-content/uploads isn’t writable by the web server user, or a newly created year/month subfolder inherited the wrong owner, the write fails.
ls -ld wp-content/uploads
ls -ld wp-content/uploads/2026/07
Directories should typically be 755 and owned by the user PHP runs as. Do not set 777 because a forum told you to — it fixes the symptom by making the folder writable by every account on the server, which on shared hosting is a genuine exposure.
Branch F — the filename
Cheapest check on the list, so do it early even though it’s the least likely. Rename the file to plain lowercase letters, numbers, and hyphens — no accents, apostrophes, ampersands, #, or non-Latin characters — and upload again. Special characters occasionally trip a security rule or break on a filesystem with mismatched encoding. Ten seconds to rule out.
What not to do
Don’t start by deactivating every plugin. It’s disruptive and it only tests Branch A and Branch D. The network response tells you more in five seconds.
Don’t add a random .htaccess handler line. The snippets circulating that add a SetHandler or AddHandler directive assume a specific host’s PHP setup. On the wrong stack they take the whole site down with a 500.
Don’t accept “it worked on retry” as fixed. Intermittent success is the signature of Branch B, and it will come back the moment the server is busy or someone uploads something bigger.
Still stuck?
If the response is a clean 200 with valid JSON and the upload still fails, the break is after the file lands — usually a plugin hooked into the attachment-processing chain throwing a fatal, or an optimizer plugin sending its own request to an external service that times out. Enable WP_DEBUG_LOG, reproduce, and run the resulting fatal through the error log decoder to see which file and which hook is actually responsible.
FAQ
Questions
What does HTTP error mean when uploading images to WordPress?
It means the browser uploader sent your file to the server and got back something it could not parse as a valid success response. The message describes the symptom, not the cause. A crashed PHP process, a firewall block, a 403, and an empty response all produce the identical three words.
Why does the same image upload fine sometimes and fail other times?
Intermittent failures almost always point to memory or CPU exhaustion rather than a configuration limit. Resizing a large photo needs a burst of memory, and on shared hosting the amount available changes with server load. A hard limit like post_max_size fails identically every single time.
Does switching WordPress from Imagick to GD fix HTTP errors?
Sometimes, and only for one specific branch. ImageMagick on shared hosts is often restricted by a policy file or low resource limits, and it fails where GD succeeds. GD uses more memory for very large files though, so switching can make memory-driven failures worse rather than better.
How do I know if my host firewall is blocking the upload?
Open your browser network tools, upload the file, and look at the status code returned for async-upload.php. A 403 or 406 with an HTML body means a web application firewall or mod_security rule rejected the request. A 500 or an empty response points at PHP crashing instead.
Will increasing upload_max_filesize fix the HTTP error?
Only if the file genuinely exceeds your limits, and post_max_size usually matters more. If the whole request body is larger than post_max_size, PHP discards it before WordPress runs and the uploader gets nothing back. Raising one without the other leaves the failure exactly where it was.
Can a filename cause a WordPress HTTP error?
Yes, though it is the rarest branch. Accented characters, apostrophes, ampersands, and non-Latin scripts can trip a security rule or break a path on a mismatched filesystem encoding. Renaming to plain lowercase letters, numbers, and hyphens costs nothing and rules the branch out in about ten seconds.