Fixing cURL Error 28 in WordPress
The number is specific and useful: 28 is always a timeout, never a refusal or a certificate problem. What matters next is which request timed out, because a loopback to your own site and a call to a payment provider need opposite fixes.
Error 28 is always a timeout, which rules out a great deal. It is not a refused connection, not a DNS failure, and not a certificate problem — those have their own numbers. Something was contacted, the wait began, and no complete reply arrived in time. The useful question is therefore not what went wrong with the connection but which request it was, because the two common answers need opposite responses.
Identify the request first
The full message usually names the URL:
cURL error 28: Operation timed out after 5001 milliseconds with 0 bytes received
Sort it into one of two categories:
| The URL is | Category | Meaning |
|---|---|---|
| Your own domain | Loopback | The site cannot reach itself |
| Anything else | External | A remote service did not answer in time |
A site health screen reporting that loopback requests cannot complete is telling you the first. A payment or shipping integration failing intermittently is telling you the second. The fixes have almost nothing in common.
Loopback timeouts
WordPress makes requests to its own address for scheduled tasks, for update checks and for the editor's own checks. The request leaves the server and comes back in through the public address, which means it passes everything a visitor's request passes — DNS, the firewall, the CDN.
Three causes account for most of these:
- DNS pointing elsewhere. A site behind a proxy resolves its own domain to the proxy, which sends the request back, and on some configurations it never lands. A host entry mapping the domain to the local address resolves it.
- A firewall blocking self-requests. Some security configurations treat a request from the server to itself as suspicious. Allowing the server's own address is the fix.
- Authentication in front of the site. A staging site behind HTTP authentication blocks its own loopbacks too, because the loopback has no credentials. This is the usual cause on a staging site and disappears in production.
The visible consequence is that scheduled tasks stop running. Posts do not publish on schedule, trash does not empty, and update checks go stale — which is the same outcome as disabling WP-Cron without configuring a replacement, reached by accident rather than on purpose.
External timeouts
Here the remote service is slow or unreachable from this server. Before changing anything, establish whether it is slow for everyone or only from here — a request from another machine to the same URL answers that in one attempt.
WordPress allows five seconds by default for many requests. For a service that legitimately takes longer, raising the limit for that specific request is reasonable:
add_filter( 'http_request_timeout', 'ti_http_timeout', 10, 2 );
function ti_http_timeout( $timeout, $url ) {
if ( str_contains( $url, 'api.example.com' ) ) {
return 20;
}
return $timeout;
}
Scoping it to one host matters. Raising the timeout globally means every failing request now occupies a PHP process for four times as long, and on a site making several of them per page load that converts a minor fault into pages that hang — which reads as a slow site rather than as a timeout.
Why raising the timeout is the wrong first move
For a loopback that is being blocked, the reply is never coming. A longer timeout means each attempt takes longer to fail, admin pages that trigger one get slower, and nothing is actually repaired. The error message looks identical, which is why this fix gets applied repeatedly to a problem it cannot touch.
Establish the category before touching the timeout. That one step prevents the most common wasted effort on this error.
Finding what is making the request
If the message names no URL, log the requests. With logging enabled through debug mode, a short snippet records every outbound call:
add_action( 'http_api_debug', 'ti_log_http', 10, 5 );
function ti_log_http( $response, $context, $class, $args, $url ) {
if ( is_wp_error( $response ) ) {
error_log( 'HTTP failed: ' . $url . ' — ' . $response->get_error_message() );
}
}
Leave it in place for a short period, then read the log. A plugin calling a service that no longer exists shows up immediately, and that is a fault to remove rather than to wait longer for. Narrowing to the responsible plugin follows the routine in fixing plugin conflicts.
Verify
For a loopback, the site health check is the direct test — it should report loopback requests completing. For an external service, trigger the integration deliberately rather than waiting for it to fail on a customer.
Check the error log afterwards even when the action appears to succeed. An integration that retries silently can be timing out on every first attempt and hiding it, which stays invisible until the retry fails too.
Frequently asked
- No. It means no complete reply arrived within the time allowed. The other end may be slow, may be unreachable from this server specifically, or may be your own site failing to answer a request it made to itself.
- A request the site makes to its own address, used for scheduled tasks and the editor's own checks. It leaves the server and has to come back in, so a firewall or DNS rule can block it while normal visitors are unaffected.
- Only for a genuinely slow remote service. Raising it for a loopback that is being blocked makes each failure take longer without ever succeeding, which turns a fast error into a slow one.