Troubleshooting

HTTP Error When Uploading Images to WordPress: The Real Causes

The red bar reading HTTP error is not a verdict on your image. It is the uploader reporting that the POST to async-upload.php came back unusable, and six unrelated failures produce exactly that. Here is how to tell them apart in about ten seconds.

HTTP Error When Uploading Images to WordPress: The Real Causes

You drag a photograph into the media library. The progress bar fills all the way to the end. Then the thumbnail is replaced by a red bar reading HTTP error and nothing else: no file name, no line number, no hint about what went wrong.

The message is not a judgement on your image. It is a report about a network request. The uploader posts the file to wp-admin/async-upload.php, waits for a small piece of JSON to come back, and if what comes back is not that JSON, it gives up and shows you those two words. The file itself may be perfectly valid and already sitting on disk.

That framing changes what you do next. You are not debugging an image, you are debugging a request that died. There are about six ways it can die, each with a different fingerprint, and your browser’s network tab identifies which one in roughly ten seconds.

The message is about the request, not the image

The exact string comes from plupload, the JavaScript uploader bundled with WordPress. In wp-includes/js/plupload/plupload.js there is a chunk handler whose failure branch retries the chunk a few times, and when the retries run out fires an event with the code plupload.HTTP_ERROR and the translated message HTTP Error., carrying the raw xhr.status, the response body and the response headers along with it.

So the condition behind the red bar is simply that the POST to async-upload.php did not come back with a usable response. A PHP fatal does that. A 500 from the process manager does that. A 403 from a firewall does that. A connection closed by a timeout does that. From the browser’s side they are indistinguishable, which is why the message says so little.

One consequence matters before you start deleting things. In wp-admin/includes/media.php, media_handle_upload() inserts the attachment row first, sends a response header X-WP-Upload-Attachment-ID, and only then calls wp_generate_attachment_metadata() to build the sub-sizes. The core comment on that header is explicit: it is used by the client to resume creating image sub-sizes after a PHP fatal error. If the request dies during resizing, the attachment post already exists. You will often find the file in the library with a broken thumbnail, which is one of the ways a library fills with half-finished attachments nobody cleans up.

Two error strings, two different failures

Modern WordPress does not always show the bare HTTP Error. string. Look in wp-includes/script-loader.php and you will find two separate messages registered for the uploader:

  • http_error: “Unexpected response from the server. The file may have been uploaded successfully. Check in the Media Library or reload the page.”
  • http_error_image: “The server cannot process the image. This can happen if the server is busy or does not have enough resources to complete the task. Uploading a smaller image may help. Suggested maximum size is 2560 pixels.”

The difference is diagnostic. The second one appears after core has already tried to rescue the upload. When the server answers with a 5xx status, wp-includes/js/plupload/handlers.js reads the attachment ID out of the X-WP-Upload-Attachment-ID response header and fires an admin-ajax call to media-create-image-subsizes, asking WordPress to finish the resizing job that just crashed. It keeps repeating that for as long as the answer is another 5xx, up to five attempts, and then sends a cleanup request and shows the “cannot process the image” message.

So: if you see the message about a smaller image and 2560 pixels, the upload itself worked and the resize is what is dying. If you see the bare HTTP Error. or the “unexpected response” wording, the request never got far enough for core to identify an attachment, which points at the request layer rather than at image processing.

The network tab is where the real error lives

Before changing any setting, get the status code. Open the browser developer tools, switch to the Network tab, leave it recording, and upload the file again. Look for the request to async-upload.php. It will be the one that goes red.

Three things on that row tell you almost everything. The status code names the layer that failed: 500 is PHP dying, 502 or 504 is the web server giving up on PHP, 403 is something refusing the request before WordPress ran, 413 is the body being rejected as too large. The time column separates a rejection from a timeout. And the response body sometimes contains the PHP fatal in plain text, because async-upload.php sends a Content-Type: text/plain header and a displayed fatal prints straight into it.

A response that ends after exactly 30 or 60 seconds is a timeout. One that returns in 40 milliseconds with a 403 was never handled by PHP at all. A 500 arriving after several seconds on a large file is usually memory. You have narrowed six causes to one before touching a configuration file.

Table of HTTP status codes seen on the async-upload.php request, with the layer that failed and where to look: 500 for a PHP fatal, 502 or 504 for a web server timeout, 403 for a firewall refusal, 413 for a body size limit, and a redirect for a site URL mismatch.

Turning on the log that catches a fatal

If the response body is empty, the fatal error is being suppressed, and you need WordPress to write it to a file instead. Three constants control this, and they belong in wp-config.php, not in a theme or a plugin. Anything that loads later than the bootstrap is too late to change error handling, which is the same reason not every snippet belongs in functions.php.

/* Put these in wp-config.php, above the
   "stop editing" comment near the bottom. */

define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );      // writes wp-content/debug.log
define( 'WP_DEBUG_DISPLAY', false ); // keep errors off the page
@ini_set( 'display_errors', 0 );

Core handles these in wp-includes/load.php. With WP_DEBUG_LOG set to true the log path is WP_CONTENT_DIR . 'https://cdn.wp-image-editor.com/debug.log'. Since WordPress 5.1 you can also pass a string, so define( 'WP_DEBUG_LOG', 'https://cdn.wp-image-editor.com/home/you/private/wp-errors.log' ) writes somewhere outside the web root, which is the better choice on a live site because a file at wp-content/debug.log is publicly readable on most hosts.

Reproduce the upload, then read the tail of that file. A memory fatal names the byte count it tried to allocate. A timeout names Maximum execution time. A crash inside the image library names class-wp-image-editor-imagick.php or its GD equivalent. Turn all three constants back off afterwards, because WP_DEBUG changes behaviour in other plugins too.

Memory exhausted while building the sub-sizes

This is the single most common cause, and the arithmetic explains why it surprises people. A JPEG is compressed. To resize it, PHP has to decompress it into a bitmap in memory, and a bitmap costs roughly width times height times four bytes. A 6,000 by 4,000 pixel photograph is a 4 MB file on disk and around 96 MB as raw pixels. Core then holds a source and a destination while it writes each registered size, so the true peak is a multiple of that.

The fingerprint is characteristic: the progress bar reaches 100 percent, there is a pause of several seconds, then the error. The upload itself succeeded. The file is on disk. It is wp_generate_attachment_metadata() that ran out of room, and because a PHP memory fatal cannot be caught, the request simply stops and the browser sees a dead response.

WordPress does try to help itself here. Both image backends call wp_raise_memory_limit( 'image' ) when they load a file, in wp-includes/class-wp-image-editor-imagick.php and wp-includes/class-wp-image-editor-gd.php. That function reads WP_MAX_MEMORY_LIMIT, passes it through the image_memory_limit filter, and tries to lift the running limit to it. Two things stop it working. It returns early if memory_limit is not changeable at runtime, which is the case on plenty of shared hosts. And WP_MAX_MEMORY_LIMIT only defaults to 256M when nothing has defined it and the existing limit is at or below that, so on a host that already sets a low fixed ceiling there is nothing to raise. Setting both explicitly is safe and reversible:

define( 'WP_MEMORY_LIMIT', '256M' );
define( 'WP_MAX_MEMORY_LIMIT', '512M' );

For reference, core’s own default for WP_MEMORY_LIMIT is 40M on a single site and 64M on multisite, which is nowhere near enough for a modern camera file. If nothing changes after adding those lines, the ceiling stopping you is below WordPress, in php.ini or in the process manager pool. There are three separate memory ceilings in a WordPress stack and they override each other in a fixed order, so knowing which one is active saves a lot of guessing.

The request ran out of time

Memory is not the only budget. max_execution_time caps how long the PHP script may run, commonly at 30 seconds, and on slow shared storage a large image with a long list of registered sizes can exceed that. Every registered size means another resample and another encode, so a theme that registers eight custom sizes does considerably more work than a bare install.

The fingerprint is the clock. Watch the network tab and note when the request dies. A failure at a suspiciously round number, 30 or 60 or 300 seconds, is a limit rather than a crash. If it is a 504 rather than a 500, the limit is in the web server or the FastCGI proxy, not in PHP, and raising max_execution_time alone will not help.

The honest fix is usually not a bigger timeout. A single upload that needs more than 30 seconds of CPU means every visitor-facing request on that host is under pressure too, and image processing is only the symptom that made it visible.

Imagick, GD, and which one is actually running

WordPress does not have one image backend, it has a list. _wp_image_editor_choose() in wp-includes/media.php runs this filter:

$implementations = apply_filters(
    'wp_image_editors',
    array( 'WP_Image_Editor_Imagick', 'WP_Image_Editor_GD' )
);

It then walks that array in order and picks the first class whose static test() passes, that supports the MIME type in question, and that implements the methods being asked for. Imagick is preferred. GD is the fallback. The chosen result is cached per set of arguments, so the decision is made once per request.

Imagick is more capable and also more fragile. It wraps a large C library with its own resource limits, its own policy file and its own opinions about certain files. A CMYK JPEG, a huge PNG, or a malformed colour profile can take the PHP worker down rather than return an error, and a dead worker looks like every other dead request from the browser’s side. Swapping to GD as a temporary diagnostic answers that in one upload:

add_filter( 'wp_image_editors', function ( $editors ) {
    return array( 'WP_Image_Editor_GD' );
} );

Put that in a small site-specific plugin, upload the same file, and remove it afterwards. If the upload now succeeds, the problem is Imagick and belongs with your host. If it still fails, you have eliminated the image library entirely. Do not leave the filter in place as a fix: GD produces noticeably softer resizes and handles modern output formats differently, so you would be trading one problem for a quieter one.

One trap deserves naming, because it wastes hours. The PHP that serves web requests and the PHP that runs your command line tools are frequently not the same binary with the same extensions. On the server this article is published from, web requests run PHP 8.5.9 with the Imagick extension 3.8.1 built against ImageMagick 6.9.12, while WP-CLI in a shell reaches PHP 8.3.6 with GD and no Imagick at all. A test upload performed through WP-CLI on that machine exercises a completely different code path from a test upload performed in the browser. If you are checking capabilities from a shell, check the version the web server uses, not the one your terminal happens to reach.

Two panels comparing the same server's PHP runtimes: web requests on PHP 8.5.9 with the Imagick extension and ImageMagick, and WP-CLI in a shell on PHP 8.3.6 with GD and no Imagick.

A security module answered before WordPress did

A large multipart POST to an admin endpoint is exactly the shape of request that web application firewalls are tuned to inspect. ModSecurity rule sets, host level bot protection and some CDN configurations will refuse it, and the refusal happens before WordPress boots. Nothing appears in debug.log, because no PHP ran.

The fingerprint is a 403 arriving almost instantly, often with an HTML error page in the response body instead of JSON. A 406 or 418 from certain rule sets means the same thing. A 413 is related but distinct: the request body exceeding a web server limit such as client_max_body_size on nginx, which caps uploads before PHP’s upload_max_filesize gets a say.

The right response is to ask your host which rule fired and have that one rule excluded for that one endpoint. Turning the firewall off to see whether it helps is a fair ten second test on staging and a bad idea on a live site. Leaving it off is not a fix.

Permissions and space under wp-content/uploads

If PHP cannot write into the uploads directory, WordPress normally returns a readable message rather than dying, so this cause is less common than the internet suggests. It still happens, usually after a migration that copied files as the wrong user.

The fingerprint is a failure on every image regardless of size, including a 20 KB icon, and often an error naming the year and month directory. Check Tools, Site Health, Info, Filesystem Permissions first, since that panel reports whether the uploads directory is writable without you touching a shell.

The repair is ownership, not permission bits. Files need to belong to the user the web server runs as, with directories at 755 and files at 644. Do not run a recursive 777 on uploads: it makes every file world writable and it does not fix a wrong owner anyway. Check free disk space and inodes too, since a full volume produces a partial write and a dead request with no error anywhere.

The URL the uploader posts to

The uploader does not guess its endpoint. It is handed one, derived from the site’s configured admin URL. If that URL does not match the address you are actually browsing, the POST goes somewhere unexpected and the response never arrives in a usable form.

The classic version is a half finished move to HTTPS: the site loads over https:// but siteurl and home still say http://, so the upload becomes a cross origin request that the browser blocks, or a redirect that loses its POST body. A mismatched www prefix does the same, as does a reverse proxy that terminates TLS without telling PHP, usually by way of a missing X-Forwarded-Proto header.

The fingerprint is unmistakable once you know it. In the network tab the request to async-upload.php shows a redirect, a CORS error in the console, or a target host that is not the one in your address bar. Fix the mismatch in Settings, General, or in the WP_SITEURL and WP_HOME constants if they are defined, and make sure every layer agrees on the scheme.

Six thousand pixels is the usual trigger

Most of the failures above are a size problem wearing a different costume. A current phone produces images between 4,000 and 8,000 pixels wide, and nothing on a website ever displays them at that size. They are simply what pushes a request past a memory or time budget that a 1,600 pixel image would never approach.

Core is aware of this. Since WordPress 5.3, wp_create_image_subsizes() in wp-admin/includes/image.php applies the big_image_size_threshold filter, default 2560, and if either dimension exceeds it, produces a scaled copy that becomes the “full” size everything else refers to. That is where the -scaled suffix in your uploads folder comes from. Your original is kept alongside it, so the disk cost goes up rather than down, and, crucially, the scaling happens after the file is on the server. It reduces the size of the copies, it does not save you the decode of the original.

Table of image dimensions against raw bitmap memory, showing a 6000 by 4000 pixel original at about 96 MB, core's 2560 pixel scaled copy at about 17 MB, a 2000 pixel resize at about 11 MB and a 1600 pixel version at about 7 MB, with three reference values underneath.

That is the whole reason resizing before upload works so reliably. A 6,000 pixel photograph reduced to 2,000 pixels before it leaves your machine needs about a ninth of the pixel memory, uploads in a fraction of the time, and never gets close to any of the limits described above. It also means core generates fewer and smaller derivatives, which matters because one upload becomes many files and every registered size multiplies the work. If a library is already full of oversized originals, WunderPaint’s image processor resizes and re-encodes existing attachments in batches in your own browser, which is a different job from stopping the next upload failing but tends to be the reason people go looking.

Most of that list can be ruled out before you upload anything, because the answer is sitting in the first few hundred bytes of the file.

Drop it below. The preflight reads the magic bytes and compares them with the extension, pulls the real dimensions out of the header, works out the memory the resize will need from the pixel count, and looks for the specific things that make WordPress answer with a bare HTTP error: a CMYK JPEG that GD refuses to open, a progressive file on an old library, six thousand pixels of width against a small memory limit, a file name full of characters that will not survive sanitisation.

Upload preflight

Drop the file that WordPress refused and this reads its first bytes here in the page: which format the bytes really hold, the pixel size taken straight from the header without decoding the image, the memory a resize would ask for, and the name as WordPress would store it. The file is only read, never uploaded, and nothing leaves this browser tab.

The file
Drop the file here
or press Enter to pick one. Only the first 512 KB are read, and only in this tab.
What the server reports

All three sit in Tools, Site Health, Info, under Server. The shorthand is the one PHP uses: 64M means 64 times 1024 KB, 0 means no time limit and -1 means no memory limit.

Nothing checked yet
Nothing checked yet Drop a file on the box above. The example file is loaded for you.
CheckWhat the bytes sayWhat to do
The name

An HTTP error with every check above green is not the file, it is the server: mod_security refusing the request, a firewall or security plugin blocking the media endpoint, or a wp-content/uploads folder the web user cannot write to. Try a small clean PNG of a few kilobytes: if that one fails too, the file was never the problem.

The sizes are read from the header only, so a file of any size is measured in a moment. Nothing is decoded, drawn or sent anywhere.

WunderPaint
The Dynamic Design and Automation Studio
WunderPaint is a layered image editor for your WordPress media library. WunderPaint Studio is the same thing in any browser, free and without an account.

Symptom to cause

Progress bar completes, then a pause of several seconds, then the error. Small images upload fine. PHP memory exhausted during sub-size generation. The file is already on disk and an attachment record probably exists. Raise WP_MEMORY_LIMIT, or resize before uploading.

The request dies at exactly 30, 60 or 300 seconds. An execution time limit, in PHP if the status is 500 and in the web server or proxy if it is 502 or 504. Confirm which by reading the status code before changing any number.

Every image fails, including a tiny PNG, and always instantly. Not a resource limit. Look at permissions on wp-content/uploads, at free disk space, or at a security rule blocking the endpoint outright.

Status 403, returned in a few milliseconds, with HTML in the response body. A firewall or bot protection module refused the POST before WordPress ran, so nothing appears in debug.log. A 413 is the neighbouring case: a body size limit in the web server.

Only certain files fail: CMYK JPEGs, very large PNGs, images from one particular camera. The Imagick backend choking on something specific. Force GD with the wp_image_editors filter as a test, then take the finding to your host.

The console shows a CORS error, a redirect, or a host that is not the one in your address bar. A site URL mismatch, usually a partial HTTPS migration or a proxy that does not pass the original scheme through.

The message mentions 2560 pixels and a busy server. That is http_error_image, which core only shows after its automatic attempts to rebuild the sub-sizes have all failed. The upload worked, the resize did not.

What to check, in order

  1. Upload a small image, around 200 KB and under 1,000 pixels wide. If it succeeds, you have a resource limit and can skip straight to memory and time. If it fails too, you have a permissions, firewall or URL problem.
  2. Open the Network tab and upload the failing file again. Record the status code, the elapsed time and the response body for the request to async-upload.php. This alone usually names the cause.
  3. Resize the failing image to 2,000 pixels on its longest edge and try again. If it uploads, the limit is confirmed and you now know roughly where it sits.
  4. Enable WP_DEBUG_LOG in wp-config.php, reproduce once, and read the tail of the log. Turn it off again afterwards.
  5. Check Tools, Site Health, Info for the PHP memory limit, the maximum execution time, the maximum upload size and the filesystem permissions that WordPress itself reports.
  6. Only now change one setting at a time, retesting between each, so you learn which one mattered.

Steps one and two are worth doing even when you are sure you know the answer. They cost under a minute and routinely overturn the obvious diagnosis, because a 403 and an out of memory fatal look identical from the media library and completely different in the network tab.

This error has such a long tail of contradictory advice online because it is not one error. It is the uploader saying a request came back unusable, and half a dozen unrelated failures produce that outcome. Every fix in every forum thread worked for the person who wrote it, on their cause, which is why applying them in sequence mostly generates new problems.

Reading the status code first collapses that ambiguity. A 500 sends you to memory and execution time. A 403 sends you to your host’s firewall. A redirect sends you to your site URLs. A failure on every file regardless of size sends you to the filesystem. Each of those has one obvious fix and no overlap with the others, and none of them requires disabling a security layer or making a directory world writable.

The long term answer is smaller inputs. Most of these limits were set to sensible values for the images websites actually need, and they only become obstacles because a modern camera file is many times larger than anything a page will ever show. Resize before you upload, keep an eye on how many sizes your theme registers, and the error stops appearing rather than being worked around.

HTTP Error When Uploading Images to WordPress: The Real Causes

Table of Contents

Learn it by building something

Every week one thing you can make the same afternoon, from dynamic templates to 3D type. Written down step by step.

One mail a week, and then it ends.
Unsubscribe in one click.

Photo Editing

Never Destroy Pixels You Might Want Back: A Working Method

Every edit is either reversible or it is not, and the difference costs nothing at the moment you make it. It shows up later, always at the worst time.

Troubleshooting

HTTP Error When Uploading Images to WordPress: The Real Causes

The red bar reading HTTP error is not a verdict on your image. It is the uploader reporting that the POST to async-upload.php came back unusable, and six unrelated failures produce exactly that. Here is how to tell them apart in about ten seconds.

Developer Tools

Cleaning an SVG Before It Goes Anywhere Near Your Site

An SVG is not an image file, it is an XML document, which is why the same file can carry a script element and several kilobytes of editor litter at once. What is actually inside one, what a sanitiser removes, why six decimal places describe nothing a screen can show, and why a missing viewBox is the reason a logo turns up at the wrong size.

Developer Tools

htaccess Explainer: Reading the File Nobody Reads

Every WordPress site on Apache has an .htaccess, and Apache reads it on every request for every directory in the path. Here is what the WordPress block actually does, why [L] does not mean last, the difference between Redirect and RewriteRule, and which pasted security snippets have done nothing since Apache 2.4.

Developer Tools

Dummy Text Generator: Lorem Ipsum That Behaves Like Real Copy

Lorem ipsum has a longest word of thirteen letters and no umlauts at all, so a layout tested with it is tested with the easiest text it will ever hold. What placeholder text should actually prove, and a generator that produces it.

Photo Editing

Remove an unwanted object from a photo without uploading it anywhere

A clone tool copies pixels from elsewhere. An inpainting model predicts what should have been behind the thing you removed, which is a different question with a much better answer on grass, hedges and brickwork. It runs in your browser, and the rest of the photograph comes back byte for byte unchanged.

Download the free WunderPaint Plugin for WordPress

The WunderPaint workspace with the layers panel, adjustment sliders, text style presets and the asset library along the bottom

The Image Editor & Design Studio

Everything described here can be done in the browser, on your own site. The live demo runs the full editor with nothing to install.

Free

Chaos Art

Autonomous painters make one-of-a-kind abstract art in 3D space - gestures, art movements, painterly media, and embeds that paint a new original for every visitor.

Pro

Particle Strokes

Paint with swarms of light: twenty-two movements, a stamp you draw yourself, and curves that give a stroke a shape - the swarm keeps painting for a few seconds after you let go.

Pro

City Diorama

Any place on earth as a miniature you could hold: real streets, water and building footprints raised into a 3D diorama - or wrapped around a sphere as your own tiny planet.

Free

Papercut Art

Layered paper pictures with real depth - a photo sliced along its actual depth into up to twenty layers, parametric landscapes, animals and clouds you can shape, letters with real counters, and a look that runs on three dials.

Pro

3D Earth Studio

A hyperrealistic globe - day and night with city lights, live clouds, atmosphere halo, country borders and highlights, click-to-place markers with flight-route arcs, satellite orbits, seamless rotation video and a live website embed.

Free

Mystic Studio

Turn a birth date into wall art - a real natal chart with houses and aspects, the moon of that night, zodiac and Chinese zodiac posters, numerology cards and a synastry wheel for two, in eight artful themes.

Free

Marble Bath

Marble paper on a virtual water bath - drop, rake and comb real Ebru patterns with gestures, flowers and classic recipes, razor-sharp at any size and re-editable as a layer.

Free

Day Ring

Turn a day into a beautiful circular schedule - colour-coded time blocks as arcs around a 24-hour clock, with concentric rings for overlaps, emoji, templates and a legend.

Free

Code Shot

Turn code into a gorgeous, share-ready image - syntax highlighting, editor themes, window frames and diff highlighting - then drop it into your design as a re-editable layer.

Pro

3D Solar System Studio

Build a date-accurate 3D solar system - real planet positions for any date, photoreal textures and one slider from artistic to true scale - then drop it into your design as an editable layer.

Pro

3D Molecule Studio

Build a real 3D molecule - from a curated library, the periodic table or a SMILES string - then style it, measure it and drop it into your design as an editable layer.

Pro

3D Textile Studio

Drop your design onto cloth that behaves like the material you pick: silk falls soft, felt holds its shape, flag fabric snaps in the wind. Hang it, blow it and drape it, then lay the finished drape back into your document as a picture.

Pro

3D Particle Studio

Point the engine at any layer and it becomes a cloud of particles that keeps its colours, flowing through a sphere, a galaxy or your own outline. Keep the frame you like as a still, or embed the running engine so it keeps moving on your page.

Free

Origami

Put your own picture on the paper and watch that very sheet fold itself into a crane or a box. Every step is a station you can stop at and turn around in 3D, which is exactly where printed diagrams leave you alone.

Pro

3D Flip Studio

A hardcover you can leaf through, a limp magazine, a strewn pile of sheets, a sticker peeling off its backing. The curl is real geometry, so the print never slides across the paper.

Free

Handwriting Fonts

Draw the alphabet here or fill in a printed sheet and photograph it. What comes out is a genuine font family, installed into your site and available in every picker.