Download Failed in WordPress: Read the Words After the Full Stop
"Download failed." is a wrapper. WordPress appends the real reason after it — a status message, a cURL error, a temp-folder problem — and each one points somewhere different: the vendor, the network, or your own server.
"Download failed." is never the whole message — the words after it are the actual error. When a plugin, theme, core or translation update cannot fetch its ZIP, WP_Upgrader::download_package() wraps whatever download_url() returned in a download_failed error, and the upgrader skin prints the wrapper followed by the original message. So you see Download failed. Not Found, Download failed. cURL error 28: …, or Download failed. A valid URL was not provided. Each suffix belongs to a different layer.
Map the suffix to the layer
| After "Download failed." | Produced by | Points to |
|---|---|---|
Not Found, Forbidden, Unauthorized, Service Unavailable |
Any non-200 status from the package host | The vendor or package URL |
cURL error 28: Operation timed out… |
No complete response within the timeout | Network or remote server |
cURL error 6: Could not resolve host / cURL error 7: … |
DNS or blocked outbound connection | Your server's network |
cURL error 60: SSL certificate problem… |
Certificate verification | TLS — see cURL error 60 |
A valid URL was not provided. |
WordPress's own URL safety check | Package host, port or DNS |
Could not create temporary file. / Destination directory for file streaming does not exist or is not writable. |
Temp file creation | Your server's temp folder |
The checksum of the file … does not match … |
Content-MD5 header mismatch |
A corrupted or altered download |
Status messages: the package URL
WordPress downloads with wp_safe_remote_get() and treats any response other than 200 as failure. It returns an error coded http_404 regardless of the actual status, with the server's reason phrase as the message — which is why a 403 still reads Forbidden, not "404".
From WordPress.org, this is rare and nearly always transient or stale: the update record points at a version that was pulled. Clear the cached update data and check again:
wp transient delete update_plugins --network
wp plugin update plugin-slug
Without WP-CLI, Dashboard › Updates › Check again does the same refresh.
From a premium vendor, Unauthorized, Forbidden or Not Found is almost always the licence: expired, activated on a different domain, or tied to a site URL that changed after a migration. Re-enter or re-activate the licence in the plugin's settings, then retry. If the vendor's server blocks your host's IP range, only the vendor can fix that.
cURL errors: the network
The download runs with a 300-second timeout, so cURL error 28 here is a genuinely stalled connection, not an impatient default. If it happens to every update, outbound HTTPS from the server is restricted or throttled; the same diagnosis as in cURL error 28 applies. cURL error 6 means the server cannot resolve the host at all. Test from the server, not your laptop:
curl -sI https://downloads.wordpress.org/plugin/akismet.zip | head -1
A 200 there and a failure in WordPress points to a PHP-level setting such as a proxy defined in wp-config.php (WP_PROXY_HOST) or a security plugin that filters HTTP requests.
"A valid URL was not provided."
This one is not a network error. wp_safe_remote_get() runs every package URL through wp_http_validate_url(), which rejects:
- hosts that resolve to private, loopback or link-local addresses (10.x, 172.16–31.x, 192.168.x, 127.x, 169.254.x);
- ports other than 80, 443 and 8080, unless the URL is on the site's own host and port;
- hostnames the server cannot resolve at all — the lookup failure surfaces as this message, not as a DNS error.
So a self-hosted update server on an internal network, or a package URL on port 8443, fails here by design. Allow it explicitly rather than weakening the check globally; for a port, the http_allowed_safe_ports filter does the same job:
add_filter( 'http_request_host_is_external', function ( $external, $host ) {
return $external || 'updates.internal.example' === $host;
}, 10, 2 );
If the host is public, check DNS from the server with getent hosts downloads.wordpress.org; a resolver that fails intermittently produces this message intermittently.
Temp folder: your server
download_url() first creates an empty temporary file with wp_tempnam(), then streams the download into it. The folder comes from get_temp_dir(): WP_TEMP_DIR if defined, otherwise PHP's temp directory, otherwise wp-content. If none is writable — common on hardened hosting with open_basedir, or after the system temp partition fills up — the download fails before a single byte arrives.
Point WordPress at a folder you control:
// wp-config.php, above "That's all, stop editing!"
define( 'WP_TEMP_DIR', __DIR__ . '/wp-content/temp' );
Create the folder, give it to the PHP user, and deny web access to it. This constant governs WordPress's own temp files only; uploads through a browser form use PHP's upload_tmp_dir, a separate setting with separate errors.
Download succeeded, install failed
If the download finished and the next line is "Unpacking the update…" (or "Unpacking the package…" on a fresh install) followed by an error, you are past this problem. A truncated or non-ZIP file fails at unpack with PCLZIP_ERR_BAD_FORMAT; permission problems fail at Could not create directory.
Common mistakes
- Reading only "Download failed." and reinstalling WordPress. The suffix was the diagnosis.
- Deleting the plugin to "reinstall it clean" when the cause is a licence: the reinstall fails the same way and the settings may go with the plugin.
- Disabling SSL verification to make a cURL error disappear.
- Retrying ten times on a
Forbiddenfrom a vendor that is rate-limiting your IP.
Verify
- The update completes from Dashboard › Updates and the plugin list shows the new version.
- A second, unrelated plugin also updates — proving the server side, not one package, was the problem.
- Automatic background updates resume; check the next morning's update email or auto-update settings.
- Following safe update practice, the site still works after the update you were blocked on.
Frequently asked
- Because the package URL WordPress was given no longer works, not because the plugin is gone. WordPress turns any non-200 response into an error carrying the server's status text, so a stale update record, a removed release or a premium vendor refusing the download all produce a short status message after Download failed.
- The update itself, yes: a ZIP uploaded from Plugins › Add New skips the download step entirely. The cause stays. If the suffix was a cURL error or a temp-folder problem, the next update fails the same way, so fix the server side as well.
- Premium plugins download from the vendor's own server, usually through a URL tied to your licence and domain. An expired licence, a site URL the licence does not cover, or a vendor server blocking your host returns a 401, 403 or 404, which shows up as Download failed followed by that status.