cURL error 28 in WordPress means your server sent an HTTP request and received no response before the timeout expired. The error string is libcurl’s CURLE_OPERATION_TIMEDOUT (code 28), and WordPress surfaces it through wp_remote_get, wp_remote_post, or the Site Health loopback test. You see it when the REST API fails, when wp-cron cannot spawn, when plugin updates stall, or when Site Health reports “The loopback request to your site failed.” The request went out. Nothing came back in time.
The most common cause is a blocked loopback request: your server is calling itself over HTTP(S), and a firewall, security plugin, or CDN rule is dropping that traffic before it reaches WordPress. This is not a timeout setting problem. Raising the timeout will not fix it. The request is being blocked, not delayed. Before touching any timeout value, confirm whether the request can reach its destination at all.
If you are mid-integration and seeing this error right now, run through the causes below in order. Most failures are resolved by Cause 1 or Cause 3. If you have already raised the timeout and the error persists, skip to Cause 1, because no amount of extra seconds fixes a request that a firewall is killing.
What Does cURL Error 28 Actually Mean?
The cURL library, which WordPress uses internally for all HTTP requests through the WP_Http class, sets two timeout values on every request handle: CURLOPT_CONNECTTIMEOUT (time to establish the connection) and CURLOPT_TIMEOUT (total time for the entire operation including data transfer). WordPress passes the same value to both, derived from the timeout argument in WP_Http::request(), which defaults to 5 seconds. When either limit is reached, libcurl aborts and returns error 28.
The error message includes the millisecond count and sometimes a byte count: “cURL error 28: Connection timed out after 5001 milliseconds with 2050 out of 2766 bytes received.” The byte count tells you whether the connection was established at all. Zero bytes received means the connection never completed. Partial bytes received means the server started responding but stalled, which points to a slow upstream or a PHP process dying mid-response.
If the message contains “Resolving timed out,” the failure is DNS, not the destination server. Your server could not resolve the hostname to an IP address within the timeout window. That is a different fix, covered in Cause 2.
Cause 1: Loopback Requests Blocked by Firewall or Security Plugin

WordPress constantly makes HTTP requests to itself. wp-cron spawns by sending a non-blocking POST to wp-cron.php on the same server. The Site Health screen runs a loopback test to site_url( 'wp-cron.php' ) with a 10-second timeout. Plugin and theme editors use loopback requests to verify changes do not break the site. When a firewall or security layer blocks these self-requests, you get cURL error 28 because the request goes out, hits a wall, and times out waiting for a response that will never come.
The blockers, in order of frequency: Cloudflare bot rules dropping origin-IP traffic, server firewalls that do not allow the server’s own public IP, security plugins (Wordfence, Solid Security) hard-blocking local requests, and HTTP Basic Authentication protecting the entire site (common on staging environments) that rejects the loopback because it carries no credentials.
How to confirm it
Go to Tools, then Site Health in wp-admin. Look for “The loopback request to your site failed.” If that message is present alongside your cURL error 28, the cause is loopback blocking. The Site Health loopback test, defined in WP_Site_Health::can_perform_loopback() in wp-includes/class-wp-site-health.php, sends a POST to wp-cron.php with a 10-second timeout and checks for a 200 response. If it fails, the error message includes the specific cURL error code.
From SSH, test the loopback directly:
curl -i –max-time 10 -X POST -d “site-health=loopback-test” https://example.com/wp-cron.php
If this returns 403, 401, or times out, the loopback is blocked. If it returns 200, the loopback works and the problem is elsewhere. Replace example.com with your actual domain.
The fix
For Cloudflare: add your server’s origin IP to the allowlist under Security, then WAF, then Tools, IP Access Rules. Set it to “Allow.” Also check that bot fight mode is not dropping requests from your server’s IP range, which Cloudflare’s heuristic flags as suspicious because the request originates from a datacenter IP.
For server firewalls (iptables, ufw, firewalld): ensure the server’s own IP is allowed. On a typical setup, the loopback goes through the public interface, not 127.0.0.1, because WordPress constructs the URL from site_url(). If your firewall blocks outbound connections to the server’s own public IP, the loopback dies. Add a rule allowing it:
iptables -A OUTPUT -d YOUR_SERVER_IP -j ACCEPT
For security plugins: temporarily deactivate the firewall or switch it to learning mode, then retest the loopback. If it succeeds, the plugin was blocking. For Wordfence, add the server’s IP to the allowlist under Firewall, then All Options, then Allowlisted IP addresses. For Solid Security, check the REST API restrictions under Security, then Settings, then WordPress Tweaks.
For Basic Auth on staging: the loopback request carries no Authorization header, so Basic Auth rejects it with 401 and the request times out. Either whitelist wp-cron.php in your .htaccess Basic Auth block, or add the Authorization header to loopback requests. WordPress core actually handles this in WP_Site_Health::can_perform_loopback() by checking for $_SERVER['PHP_AUTH_USER'] and $_SERVER['PHP_AUTH_PW'] and including them in the loopback request headers, but this only works for the Site Health test. wp-cron spawning does not include these credentials.
To whitelist wp-cron.php in .htaccess Basic Auth, add this above the WordPress rewrite rules:
<Files wp-cron.php>
Require all granted
</Files>
This removes Basic Auth protection from wp-cron.php specifically. Since wp-cron.php triggering does not expose sensitive data, this is an acceptable tradeoff for staging environments. Do not do this on production without assessing your security posture.
Where this fix does not apply
This fix does not apply when the failing request is to an external domain, not to your own site. If your plugin is calling https://api.example.com/endpoint and timing out, the problem is outbound connectivity or the remote server, not loopback blocking. Skip to Cause 2 or Cause 4. This fix also does not apply if your host blocks outbound HTTP entirely at the platform level, which some managed WordPress hosts do. In that case, no firewall rule you configure will help. Contact your host.
Cause 2: DNS Resolution Delays
If the error message contains “Resolving” or the request fails before any bytes are received, DNS resolution is the bottleneck. Your server’s DNS resolver cannot look up the destination hostname fast enough. WordPress has no control over DNS resolution speed. It happens inside libcurl before the HTTP connection even opens.
How to confirm it
From SSH, time the DNS lookup directly:
dig example.com +stats | grep “Query time”
If the query time exceeds 1000ms, your DNS resolver is slow. For loopback requests, test resolution of your own domain:
dig $(hostname -f) +stats | grep “Query time”
Also check which resolvers your server uses:
cat /etc/resolv.conf
If the listed nameservers are slow public resolvers or, worse, resolvers that no longer respond, every HTTP request WordPress makes pays the penalty. A 5-second WordPress timeout can be consumed entirely by DNS resolution, leaving zero time for the actual HTTP connection.
The fix
Switch to a faster DNS resolver. Edit /etc/resolv.conf and replace the nameserver entries. For most servers, Google’s public DNS (8.8.8.8, 8.8.4.4) or Cloudflare’s (1.1.1.1) resolves in under 50ms from most datacenters. For loopback requests specifically, add a static hosts entry so the server never needs DNS for its own domain:
echo “YOUR_SERVER_IP example.com www.example.com” >> /etc/hosts
This eliminates DNS resolution entirely for loopback requests. The server resolves its own domain from /etc/hosts instantly. Use this only for your own domain, not for external APIs.
If /etc/resolv.conf is managed by systemd-resolved or NetworkManager, your changes will be overwritten on reboot. On systemd-resolved systems, edit /etc/systemd/resolved.conf and set DNS=8.8.8.8, then run systemctl restart systemd-resolved. On NetworkManager systems, use nmcli to set the DNS for your connection.
Where this fix does not apply
If dig returns a fast query time (under 50ms) and the error message does not mention “Resolving,” DNS is not your problem. Also, if your site is behind Cloudflare and the loopback goes through Cloudflare’s proxy IP, the hosts file entry approach breaks Cloudflare routing. In that case, fix DNS at the resolver level only.
Cause 3: Host-Level Timeouts Killing the Request Early

WordPress sets a 5-second default timeout. You raise it to 30 seconds. The request still times out at 10 seconds. This happens because PHP, Nginx, Apache, or PHP-FPM kills the process before WordPress’s timeout is reached. The WordPress timeout is the ceiling for the HTTP request, but the PHP execution timeout is the ceiling for the entire script, and the server-level proxy timeout is the ceiling for the entire connection. If those ceilings are lower than your WordPress timeout, raising the WordPress timeout changes nothing.
The layers, from outer to inner
Nginx proxy_read_timeout (default 60s) applies if Nginx proxies to PHP-FPM. For loopback requests that go through Nginx, this is the outermost ceiling. If Nginx has a custom proxy_read_timeout set to 10s, no WordPress filter can extend the request beyond that.
Apache Timeout directive (default 300s in many builds, but shared hosts often lower it) governs how long Apache waits for a response. For mod_php, this is the ceiling. For PHP-FPM behind Apache, the ProxyTimeout directive applies.
PHP max_execution_time (default 30s) limits how long the PHP script can run. WordPress HTTP requests are blocking: the PHP process sits and waits for the HTTP response. If max_execution_time is 30 and your HTTP timeout is 60, PHP will abort the script at 30 seconds and you will see a fatal error, not a cURL error. But on some configurations, particularly PHP-FPM with request_terminate_timeout set in the pool config, the FPM process manager kills the process at the configured timeout and the HTTP request is aborted, producing a cURL error 28.
How to confirm it
Check your PHP timeout:
php -i | grep max_execution_time
Check your PHP-FPM pool timeout (if applicable):
grep request_terminate_timeout /etc/php/*/fpm/pool.d/www.conf
Check Nginx proxy timeouts:
grep -r “proxy_read_timeout|proxy_connect_timeout” /etc/nginx/
If any of these values are lower than your WordPress HTTP timeout, that layer is killing the request first.
The fix
Raise the PHP execution time in php.ini for the specific request. In the WordPress context, you can set it in wp-config.php before the require_once( ABSPATH . 'wp-settings.php' ) line:
ini_set( ‘max_execution_time’, ‘120’ );
For PHP-FPM, also check request_terminate_timeout in the pool configuration, typically at /etc/php/8.x/fpm/pool.d/www.conf. If it is set to 30 or 60, raise it or comment it out to inherit the max_execution_time value. After changing, reload PHP-FPM:
sudo systemctl reload php8.x-fpm
For Nginx, raise the proxy timeouts in your server block or location block:
proxy_connect_timeout 60s;
proxy_read_timeout 120s;
proxy_send_timeout 120s;
After changing, test and reload:
sudo nginx -t && sudo systemctl reload nginx
Where this fix does not apply
If you are on shared hosting and do not have access to php.ini, Nginx config, or PHP-FPM pool settings, you cannot apply this fix. Some shared hosts allow ini_set in wp-config.php for max_execution_time, but request_terminate_timeout and Nginx proxy timeouts are host-level and cannot be overridden from within WordPress. Contact your host to raise these limits, or move to a host that gives you control over server configuration.
Cause 4: WordPress Default 5-Second Timeout Too Short
WordPress sets a 5-second default timeout for all HTTP requests. This is defined in WP_Http::request() in wp-includes/class-wp-http.php, where the default arguments array includes 'timeout' => apply_filters( 'http_request_timeout', 5, $url ). The value 5 is passed through the http_request_timeout filter, which means you can change it. The cURL transport then converts this to seconds and sets both CURLOPT_CONNECTTIMEOUT and CURLOPT_TIMEOUT using ceil(), because cURL expects integers and a value of 0 means unlimited.
Five seconds is enough for most requests to fast, local endpoints. It is not enough for requests to slow external APIs, to endpoints behind rate limiting, or to your own site when the PHP stack takes 3 seconds to bootstrap before responding. If the destination legitimately needs 8 seconds to respond, the default 5-second timeout kills the request before the response arrives.
How to Raise the Timeout With http_request_timeout

The http_request_timeout filter lets you override the default 5-second timeout. Add a filter callback to a must-use plugin (recommended) or your theme’s functions.php. The filter receives two parameters: the current timeout value (float, in seconds) and the request URL (string). The URL parameter was added in WordPress 5.1.0.
To raise the timeout for all requests globally:
add_filter( ‘http_request_timeout’, function( $timeout, $url ) {
return 30;
}, 10, 2 );
This sets every HTTP request WordPress makes to a 30-second timeout. Do not use this approach blindly. A 30-second timeout on a blocking request means a PHP process sits idle for up to 30 seconds waiting for a response. On a busy site, this consumes worker processes and can cause cascading timeouts.
The better approach is to raise the timeout only for the specific URL or domain that needs it:
add_filter( ‘http_request_timeout’, function( $timeout, $url ) {
if ( false !== strpos( $url, ‘api.example.com’ ) ) {
return 30;
}
return $timeout;
}, 10, 2 );
This leaves the default 5-second timeout for all other requests and only extends it for requests matching the domain you specify. This is the correct pattern when a specific external API is slow but your other HTTP requests are fine.
For loopback requests specifically, WordPress also provides the http_request_loopback_timeout filter, but this is not a separate filter. The loopback timeout is governed by the same http_request_timeout filter. However, wp-cron spawning uses a separate, hardcoded 0.01-second non-blocking timeout in spawn_cron(), which you cannot change via filter. If your cURL error 28 comes from Site Health’s loopback test, that test uses a hardcoded 10-second timeout in WP_Site_Health::can_perform_loopback(), also not filterable.
You can also set the timeout directly on a specific wp_remote_get or wp_remote_post call by passing it in the arguments array, which overrides the filter:
$response = wp_remote_get( ‘https://api.example.com/endpoint’, array( ‘timeout’ => 30 ) );
This is the cleanest approach when you control the code making the request. The per-call timeout argument takes precedence over the filter default. Use this in your own plugin or theme code instead of the global filter when possible.
Where this fix does not apply
Raising the WordPress timeout does not help if the request is being blocked (Cause 1), if DNS resolution is consuming the entire timeout window (Cause 2), or if a host-level timeout kills the PHP process before the WordPress timeout is reached (Cause 3). It only helps when the destination server legitimately needs more time to respond and no upstream layer is killing the request first. If you raise the timeout from 5 to 30 and the error changes from “timed out after 5001 milliseconds” to “timed out after 30001 milliseconds,” you have confirmed the request is being blocked or the destination is genuinely unresponsive. More time will not help.
Where Raising the Timeout Only Hides a Slower Problem
This is the failure mode most developers miss. You raise the timeout, the error disappears, and you move on. The request is now taking 25 seconds instead of failing at 5. It is not faster. It is slower. You have removed the error message without addressing why the request takes 25 seconds in the first place.
If a loopback request to wp-cron.php takes 25 seconds, something is wrong with your server’s PHP stack. WordPress bootstrapping should take under 1 second on a properly configured server. A 25-second loopback means your database is slow, your wp_options table has bloated autoloading rows, a plugin is doing expensive work on init, or your server is CPU-constrained. Raising the timeout from 5 to 30 lets the cron spawn succeed, but every page load that triggers cron spawning now waits longer, degrading your site’s time to first byte.
If an external API request takes 25 seconds, the API is either rate-limiting you, experiencing an outage, or returning a response so large that transfer time dominates. Raising the timeout lets the request complete, but you are paying 25 seconds of PHP process time per request. If this runs in a cron job or a user-facing page load, it will consume resources and eventually cause different failures.
The diagnostic question to ask after raising the timeout: how long does the request actually take? Measure it:
$start = microtime( true );
$response = wp_remote_get( $url, array( ‘timeout’ => 60 ) );
$elapsed = microtime( true ) – $start;
error_log( “HTTP request to $url took {$elapsed} seconds” );
Check wp-content/debug.log after the request. If it reports 25 seconds, you have not fixed the problem. You have made it silent. The underlying cause is still there, and it will resurface as degraded performance, exhausted PHP workers, or database connection limits.
How to Confirm cURL Error 28 Is Fixed
Run these checks in order. Each one confirms a different layer.
Step 1: Test the loopback from SSH
curl -i –max-time 10 -X POST -d “site-health=loopback-test” https://example.com/wp-cron.php
You should see HTTP/2 200 with a small response body. If you get 403, 401, or a timeout, the loopback is still blocked. Return to Cause 1.
Step 2: Check Site Health
Go to Tools, then Site Health in wp-admin. The “Loopback requests” test should show “The loopback request to your site completed successfully.” If it still shows the error, the fix is not applied or the blocker is still active. The REST API test should also pass. If the REST API test fails with cURL error 28 but the loopback test passes, the problem is specific to the REST API endpoint, which may point to a security plugin blocking REST writes specifically. See our guide on fixing the rest_cannot_create error for that diagnostic path.
Step 3: Verify wp-cron spawns
If you have SSH and WP-CLI, run:
wp cron test –path=/var/www/yourdomain.com
This tests whether the cron system can spawn HTTP requests. If it returns success, cron spawning is working. If it returns an error, the loopback is still blocked. See our guide on wp-cron not working fixes for the full diagnostic, including how to switch to a real system cron that bypasses the HTTP loopback entirely.
Step 4: Test the original failing request
Re-run whatever was failing. If it was a REST API call, test with curl:
curl -i -u “username:app_password” https://example.com/wp-json/wp/v2/users/me
If this returns 200 with JSON, the REST API is reachable and authentication is working. If you get a 401 instead of cURL error 28, the timeout is fixed but authentication is now the problem. See our guide on WordPress application passwords not working for that fix. If you get a CORS error in the browser but the curl request succeeds, the problem has moved to the browser layer. See our WordPress REST API CORS errors guide.
Step 5: Confirm the request time is reasonable
Measure the elapsed time of the request using the microtime() logging snippet from the previous section. A healthy loopback should complete in under 2 seconds. A healthy external API call should complete in under 5 seconds. If the request takes 20+ seconds and succeeds only because you raised the timeout, you have masked the problem, not fixed it.
A Note on Server-to-Server Integrations
If you are seeing cURL error 28 when an external tool tries to publish content to your WordPress site, the failure mode is different from a loopback problem. The request comes from outside, not from the site calling itself. In that case, Cause 1 (loopback blocking) does not apply. Check whether your server firewall blocks inbound connections on port 443 from the tool’s IP, whether Cloudflare is dropping the request, or whether PHP is timing out during the REST API request. ClearPost, our WordPress plugin, makes server-to-server requests to the REST API using application passwords, which avoids the loopback failure mode entirely since the request originates from an external server, not from the site to itself. But if your server blocks inbound REST API traffic, that request will also time out, and no timeout filter on the WordPress side will fix it. The fix in that case is firewall configuration, not WordPress code.
Try ClearPost free for 7 days. AI does the heavy lifting, you approve every post before it goes live. No long onboarding, no agency overhead, cancel anytime. See what 30 SEO-optimized posts a month looks like compared to the manual cycle you are running now.
Frequently Asked Questions
What does cURL error 28 mean in WordPress?
It means WordPress sent an HTTP request using the cURL library and did not receive a response before the timeout expired. The error is libcurl’s CURLE_OPERATION_TIMEDOUT (code 28). WordPress surfaces it through wp_remote_get, wp_remote_post, the Site Health loopback test, or wp-cron spawning. The error message includes the timeout duration in milliseconds and sometimes a byte count showing how much data was received before the timeout.
Why do loopback requests fail with cURL error 28 on the same server?
WordPress makes HTTP requests to itself for wp-cron spawning, Site Health checks, and plugin/theme editor verification. These loopback requests go through the server’s public IP, not 127.0.0.1, because WordPress constructs the URL from site_url(). If a firewall, CDN rule (especially Cloudflare bot fight mode), security plugin, or HTTP Basic Auth on the entire site blocks the request, it times out. The fix is to allowlist the server’s own IP in your firewall and security plugin, or whitelist wp-cron.php from Basic Auth protection.
How do DNS resolution delays cause cURL error 28?
If the error message contains “Resolving,” the server’s DNS resolver could not look up the hostname within the timeout. WordPress has no control over DNS speed, it happens inside libcurl before the HTTP connection opens. A 5-second WordPress timeout can be consumed entirely by DNS resolution. Test with dig example.com +stats. If query time exceeds 1000ms, switch to a faster resolver like 8.8.8.8 or 1.1.1.1 in /etc/resolv.conf. For loopback requests, add a static hosts entry in /etc/hosts to skip DNS entirely for your own domain.
What host-level timeouts can cause cURL error 28?
PHP max_execution_time (default 30s), PHP-FPM request_terminate_timeout, Nginx proxy_read_timeout (default 60s), and Apache ProxyTimeout or Timeout directive. If any of these are set lower than your WordPress HTTP timeout, that layer kills the request before WordPress’s timeout is reached. Raising the WordPress timeout via the http_request_timeout filter changes nothing because the process dies first. Check each layer with php -i | grep max_execution_time, grep request_terminate_timeout in your FPM pool config, and grep proxy_read_timeout in your Nginx config.
How do I raise the timeout using the http_request_timeout filter?
Add a filter callback to a must-use plugin or your theme’s functions.php. The filter receives the current timeout (float, in seconds) and the URL (string). To raise it globally: add_filter( ‘http_request_timeout’, function( $timeout, $url ) { return 30; }, 10, 2 ). To raise it only for a specific domain, check the $url parameter with strpos and return the higher value only for matching URLs. You can also pass a timeout argument directly in the wp_remote_get or wp_remote_post args array, which overrides the filter.
When does raising the timeout only hide a slower problem?
Raising the timeout fixes the error message but not the underlying cause when the request is being blocked (the extra time makes no difference), when DNS resolution is consuming the entire window (more time just means more time spent on DNS), or when a host-level timeout kills the process first (the WordPress timeout never takes effect). It also masks performance problems: if a loopback request takes 25 seconds, raising the timeout to 30 lets it succeed, but every page load that triggers cron spawning now waits 25 seconds. Measure the actual request time with microtime() logging. If it exceeds 5 seconds for a loopback or 10 seconds for an external call, the root cause is still unfixed.
Frequently Asked Questions
What does cURL error 28 mean in WordPress?
It means WordPress sent an HTTP request using the cURL library and did not receive a response before the timeout expired. The error is libcurl’s CURLE_OPERATION_TIMEDOUT (code 28). WordPress surfaces it through wp_remote_get, wp_remote_post, the Site Health loopback test, or wp-cron spawning. The error message includes the timeout duration in milliseconds and sometimes a byte count showing how much data was received before the timeout.
Why do loopback requests fail with cURL error 28 on the same server?
WordPress makes HTTP requests to itself for wp-cron spawning, Site Health checks, and plugin or theme editor verification. These loopback requests go through the server’s public IP, not 127.0.0.1, because WordPress constructs the URL from site_url(). If a firewall, CDN rule (especially Cloudflare bot fight mode), security plugin, or HTTP Basic Auth on the entire site blocks the request, it times out. The fix is to allowlist the server’s own IP in your firewall and security plugin, or whitelist wp-cron.php from Basic Auth protection.
How do DNS resolution delays cause cURL error 28?
If the error message contains Resolving, the server’s DNS resolver could not look up the hostname within the timeout. WordPress has no control over DNS speed, it happens inside libcurl before the HTTP connection opens. A 5-second WordPress timeout can be consumed entirely by DNS resolution. Test with dig example.com plus the stats flag. If query time exceeds 1000ms, switch to a faster resolver like 8.8.8.8 or 1.1.1.1 in /etc/resolv.conf. For loopback requests, add a static hosts entry in /etc/hosts to skip DNS entirely for your own domain.
What host-level timeouts can cause cURL error 28?
PHP max_execution_time (default 30s), PHP-FPM request_terminate_timeout, Nginx proxy_read_timeout (default 60s), and Apache ProxyTimeout or Timeout directive. If any of these are set lower than your WordPress HTTP timeout, that layer kills the request before WordPress’s timeout is reached. Raising the WordPress timeout via the http_request_timeout filter changes nothing because the process dies first. Check each layer with php -i grep max_execution_time, grep request_terminate_timeout in your FPM pool config, and grep proxy_read_timeout in your Nginx config.
How do I raise the timeout using the http_request_timeout filter?
Add a filter callback to a must-use plugin or your theme’s functions.php. The filter receives the current timeout (float in seconds) and the URL (string). To raise it globally, return 30 from the callback. To raise it only for a specific domain, check the URL parameter with strpos and return the higher value only for matching URLs. You can also pass a timeout argument directly in the wp_remote_get or wp_remote_post args array, which overrides the filter.
When does raising the timeout only hide a slower problem?
Raising the timeout fixes the error message but not the underlying cause when the request is being blocked, when DNS resolution is consuming the entire window, or when a host-level timeout kills the process first. It also masks performance problems: if a loopback request takes 25 seconds, raising the timeout to 30 lets it succeed, but every page load that triggers cron spawning now waits 25 seconds. Measure the actual request time with microtime logging. If it exceeds 5 seconds for a loopback or 10 seconds for an external call, the root cause is still unfixed.
