Root Cause Diagnosis
The “HTTP error” message is the Media Uploader’s generic fallback. It appears when the request to wp-admin/async-upload.php returns anything other than the expected JSON response: a non-200 status, a dropped connection, or a body corrupted by PHP warnings. Failures that occur before WordPress finishes loading are never written to wp-content/debug.log. These include a request rejected by the web server for body size, a PHP-FPM worker killed by the OOM killer or a timeout, and a WAF block. When debug.log stays empty, the failing layer is the web server, PHP-FPM, the kernel or a proxy, not WordPress core.
Quick Fix
The most common cause is an Imagick crash or memory spike during thumbnail generation. Force the GD image editor with a must-use plugin:
<?php
// wp-content/mu-plugins/force-gd.php
add_filter( 'wp_image_editors', function () {
return array( 'WP_Image_Editor_GD' );
} );
If the upload now succeeds, keep Imagick off or apply the thread-limit fix in Step 5. If it still fails, continue with the steps below.
Step-by-Step Resolution
Step 1: Identify the failing layer from the HTTP response.
Open browser DevTools, go to the Network tab, upload a file, and select the async-upload.php request. The status code decides the branch:
| Status | Likely layer | Go to |
|---|---|---|
| 413 | Web server body limit | Step 4 |
| 500 / 502 / 504 | PHP fatal, FPM crash or timeout | Steps 3, 4, 5 |
| 403 | ModSecurity / WAF | Step 7 |
| 200 with non-JSON body | PHP warnings printed into the response | Step 2 |
| Cancelled / (failed) | Connection reset, proxy cutoff | Steps 3, 7 |
Step 2: Enable logging correctly in wp-config.php.
Add these constants above the line /* That's all, stop editing! Happy publishing. */:
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
@ini_set( 'display_errors', 0 );
WP_DEBUG_DISPLAY must be false. If it is true, any warning is printed into the AJAX response and itself triggers “HTTP error”. To log to an explicit path, use define( 'WP_DEBUG_LOG', '/var/log/wp-debug.log' ); and make sure the PHP user can write to it.
The log is written to wp-content/debug.log. If the constants are ignored, check that they are not defined twice (the first definition wins), that no mu-plugin or host-level config overrides them, and that wp-content/ is writable by the PHP user.
Step 3: Read the server-side logs when debug.log is empty.
# Nginx
tail -n 100 /var/log/nginx/error.log
# Apache
tail -n 100 /var/log/apache2/error.log
# PHP-FPM (adjust version)
tail -n 100 /var/log/php8.2-fpm.log
journalctl -u php8.2-fpm --since "15 min ago"
# Kernel OOM killer
dmesg -T | grep -i -E "killed process|out of memory"
Patterns to look for:
client intended to send too large bodyindicates an Nginx body limit (Step 4).upstream timed outorrequest_terminate_timeoutindicates a timeout (Step 4).Allowed memory size of ... exhaustedindicatesmemory_limit(Step 4).child exited on signal 11orsignal 9indicates a segfault or OOM kill, often Imagick (Step 5).ModSecurity: Access deniedindicates a WAF rule (Step 7).
Step 4: Raise upload, memory and timeout limits at every layer.
PHP (/etc/php/8.2/fpm/php.ini, or .user.ini in the WordPress root on shared hosting):
upload_max_filesize = 64M
post_max_size = 64M
memory_limit = 256M
max_execution_time = 300
max_input_time = 300
post_max_size must be greater than or equal to upload_max_filesize. Restart PHP-FPM afterwards:
systemctl restart php8.2-fpm
Nginx (/etc/nginx/nginx.conf or the site vhost):
client_max_body_size 64M;
location ~ \.php$ {
fastcgi_read_timeout 300;
fastcgi_send_timeout 300;
}
nginx -t && systemctl reload nginx
Apache with mod_fcgid (/etc/apache2/mods-enabled/fcgid.conf):
FcgidMaxRequestLen 67108864
WordPress memory (wp-config.php):
define( 'WP_MEMORY_LIMIT', '256M' );
define( 'WP_MAX_MEMORY_LIMIT', '512M' );
Step 5: Fix Imagick threading and memory crashes.
Imagick can spawn multiple threads that exceed per-process limits on constrained hosts. Limit it at the top of wp-config.php:
putenv( 'MAGICK_THREAD_LIMIT=1' );
Alternatively, in .htaccess (Apache with mod_env):
SetEnv MAGICK_THREAD_LIMIT 1
Memory use during resizing scales with pixel dimensions, not file size. A 6000×4000 JPEG needs roughly 100 MB or more for decoding alone, regardless of its size on disk. Reduce the threshold for downscaling large originals:
add_filter( 'big_image_size_threshold', function () { return 2560; } );
Step 6: Verify filesystem permissions, space and temp directory.
# Ownership and mode
stat -c '%U:%G %a' wp-content/uploads
# Standard modes
find wp-content/uploads -type d -exec chmod 755 {} \;
find wp-content/uploads -type f -exec chmod 644 {} \;
# Free space and inodes
df -h /var/www /tmp
df -i /var/www /tmp
# PHP temp upload directory
php -r 'echo ini_get("upload_tmp_dir") ?: sys_get_temp_dir(), PHP_EOL;'
The directory must be owned by the PHP-FPM user (commonly www-data). A full /tmp or exhausted inodes produces the same “HTTP error”.
Step 7: Isolate the layer with WP-CLI, then eliminate plugins and WAF.
Bypass the browser and web server entirely:
wp media import /path/to/test.jpg --allow-root
- If this succeeds, PHP and image processing are fine. The fault is in the web server, proxy, WAF or a plugin hooking the admin request.
- If this fails, the error output points directly at the PHP, GD/Imagick or filesystem problem.
Then rule out plugins and the theme:
wp plugin deactivate --all
wp theme activate twentytwentyfour
Re-enable plugins one at a time to find the culprit. Security, image-optimization and CDN-offload plugins are the most common offenders. For ModSecurity, check /var/log/modsec_audit.log for the rule ID, then whitelist that rule for /wp-admin/async-upload.php rather than disabling ModSecurity globally.
Why This Fails for Some Users
- Cloudflare or reverse proxy limits. Cloudflare Free and Pro plans cap request bodies at 100 MB, and a response that takes more than about 100 seconds returns 524. Uploads larger than the cap fail before reaching the origin, so no origin log entry exists. Test by uploading directly against the origin IP or with the proxy disabled.
php_valuein .htaccess under PHP-FPM.php_valueandphp_flagdirectives are only valid formod_php. Under FPM or CGI they cause a 500 error. Use .user.ini instead..user.inicaching. PHP re-reads.user.inionly everyuser_ini.cache_ttlseconds (default 300). Changes may appear to have no effect for up to five minutes.- Control panels overriding PHP settings. Hosting panels often keep per-site PHP overrides separate from php.ini. Editing the global file does nothing if a per-site setting overrides it. Change the value in the panel’s PHP settings for that site, or in its generated pool config under /etc/php/8.2/fpm/pool.d/.
open_basedirrestrictions. Ifopen_basedirexcludes the temp directory orwp-content/uploads, the move from the temp upload path fails. Verify withphp -i | grep open_basedir.- SELinux. On RHEL-family systems, upload directories need the correct context. Run
ls -Z wp-content/uploadsand applychcon -R -t httpd_sys_rw_content_t wp-content/uploads(or a persistentsemanage fcontextrule). Denials appear in /var/log/audit/audit.log, not in PHP logs. - Unsupported image formats or color profiles. HEIC, CMYK JPEGs, and WebP or AVIF files on a build without the matching codec can crash Imagick or GD. Test with a plain sRGB JPEG to confirm.
- Low
pm.max_childrenorpm.max_requestsrecycling. Under FPM, a worker recycled mid-upload or a pool with too few workers drops the request. Check /etc/php/8.2/fpm/pool.d/www.conf and the FPM log forserver reached pm.max_children. - Filenames with special characters. Non-ASCII or very long filenames can trip WAF rules or filesystem limits. Rename the file and retry.
- Persistent object cache or CDN-offload plugins. A misconfigured offload plugin can fail after the local save, which still surfaces as “HTTP error”. The WP-CLI test in Step 7 exposes this when
wp media importfails only with the plugin active.