Skip to content
  • FG-66
  • 13 min read
  • Last checked November 8, 2026

How to Fix “Your site could not complete a loopback request” and “The REST API encountered an error” in Site Health

Site Health flags a failed loopback request or a REST API error. Learn what the test does, test it from the server with curl, and fix DNS, firewall, auth and plugin causes.

Run the health check

Tools > Site Health lists a critical issue: “Your site could not complete a loopback request”, or “The REST API encountered an error”. Expand it and you see a detail line such as “Error: cURL error 28: Operation timed out after 10001 milliseconds with 0 bytes received (http_request_failed)”, “cURL error 6: Could not resolve host”, or “REST API Response: (401) Unauthorized”. Sometimes the warning is milder: “The REST API encountered an unexpected result” or a loopback that “returned an unexpected http status code”.

Both tests ask one question: can your server call its own website over the network and get a normal answer? When it cannot, nothing looks broken to visitors, but scheduled tasks, the code editors and some background jobs quietly stop working. This guide is for site owners, designers and developers who want to know why the server cannot reach itself, and how to fix it at the right layer.

What is happening

A loopback request is an HTTP request that WordPress, running in PHP on your server, sends to your own site’s public URL. It leaves PHP, looks up your domain in DNS (or the server’s hosts file), connects to whatever IP that returns, negotiates HTTPS, and arrives back at your web server as if it came from a visitor. A second PHP worker then handles it.

Here is what the two Site Health tests actually send, based on the WordPress core code:

Test Request Passes when
Loopback POST to wp-cron.php, with your browser’s cookies, a 10 second timeout, and basic auth credentials if PHP can see them HTTP 200 comes back
REST API GET to /wp-json/wp/v2/types/post?context=edit, with your cookies, a REST nonce and the same timeout HTTP 200 comes back with JSON describing the post type, including capabilities

A transport failure (DNS, connection refused, timeout, TLS) is reported as critical, with a cURL error. A response with the wrong status code is reported as a recommendation, with the code. Both tests turn certificate verification off by default (through the https_local_ssl_verify filter, which defaults to false), so a self-signed certificate on staging is usually not the cause unless some code switches that filter on.

If your problem is the block editor saying “Updating failed. The response is not a valid JSON response.”, that is a different symptom with its own causes, and our guide to that block editor error covers it. This guide is about the server-to-itself path.

Before you start

You need wp-admin access and SSH access to the web server; loopback problems can only be tested properly from the machine that makes the request. On shared hosting without SSH, you can still work through Steps 1, 4, 5 and 6, and send the rest to your host. Nothing in the diagnosis changes your site. Back up any file before you edit it (.htaccess, an Nginx config, /etc/hosts).

How to run the commands in this guide. Server commands run in an SSH terminal on the web server, from your site folder, the one that contains wp-config.php (often public_html). WP-CLI is called wp on most servers; some hosts name it wp-cli, so swap the name if wp is not found. Commands marked “your computer” use curl: macOS and Linux have it built in, and Windows 10 and 11 include curl.exe in PowerShell, or you can use WSL.

Step 1: Read the error detail

The detail line under the Site Health item narrows the cause before you run anything.

Detail Meaning Go to
cURL error 6: Could not resolve host The server cannot look up its own domain Step 3
cURL error 7: Failed to connect … Connection refused The IP it resolved to refuses the connection, often a NAT or firewall issue Step 3
cURL error 28: Operation timed out Packets go nowhere (firewall, NAT hairpin), or the request reached PHP and got stuck (sessions, busy workers) Steps 3 and 6
cURL error 35 or 60 35 is a failed TLS handshake (protocol or cipher mismatch); 60 is a certificate that failed verification, possible only if verification was switched on Advanced
401 Unauthorized Password protection (basic auth) or a plugin requiring login Step 4
403 Forbidden A WAF, ModSecurity, a security plugin or Cloudflare blocked the request Step 5
404 Not Found (REST) The REST route is not reachable: permalinks or rewrite rules, or a plugin disabling the REST API Step 5
500 or 503 The request reached WordPress and something crashed or a maintenance mode answered Step 6

Step 2: Reproduce it from the server

Run the same kind of request yourself, from the server, so you can watch it.

curl -I https://example.com
curl -sS -o /dev/null -w "%{http_code} %{remote_ip} %{time_total}s\n" -X POST https://example.com/wp-cron.php
getent hosts example.com

Run these over SSH on the web server. Replace example.com with the hostname in your WordPress Address (Settings > General), exactly, including www. if it has one.

  • curl -I sends a HEAD request and prints the response headers. Good output starts with HTTP/2 200 or HTTP/1.1 200. A 401, 403 or 503 first line is your answer.
  • The second command mimics the loopback test: -X POST sends a POST, -o /dev/null discards the body, and -w prints the status code, the IP curl actually connected to and the total time. Good: 200, your server’s IP, well under a second. Bad: a Cloudflare IP with 403, an old server’s IP, or a long hang ending in a timeout.
  • getent hosts shows the IP your server resolves the name to, the same way PHP will, including any entry in /etc/hosts. Compare it with your server’s real IP from the hosting panel.

Then test through WordPress’s own HTTP layer, which uses the same code path as Site Health:

wp eval '$r = wp_remote_post( site_url( "wp-cron.php" ), array( "timeout" => 10 ) ); echo is_wp_error( $r ) ? $r->get_error_message() : wp_remote_retrieve_response_code( $r ); echo "\n";'

Run this over SSH from the site folder. There are no placeholders: site_url() fills in your own address. It prints either the HTTP status code or the transport error message. Good output is 200. Anything else matches a row in the Step 1 table. This request does not carry your browser’s cookies, so if it returns 200 while Site Health fails, look at sessions and login-dependent plugins in Step 6.

Step 3: Fix DNS, NAT hairpin, hosts file and firewall problems

Bypass DNS to prove the point

curl -sS -o /dev/null -w "%{http_code} %{time_total}s\n" --resolve example.com:443:LOCAL_IP https://example.com/wp-cron.php -X POST

Run this over SSH on the web server. LOCAL_IP is 127.0.0.1 if the web server listens on all addresses, or the server’s private or public IP if it listens on a specific one (shown in your panel or with ip addr). --resolve tells curl to connect to that IP for example.com on port 443 without asking DNS, while still sending the right hostname for HTTPS and virtual hosting.

If this returns 200 quickly but the plain request in Step 2 failed, the web server is fine and the problem is getting there: the name resolves to an address the server cannot reach from inside, or to something in between that blocks it.

Common causes on this path

  • NAT hairpin. Servers behind a NAT router or some cloud networks resolve the domain to the public IP, but the network does not route traffic from inside back to its own public address. The result is a timeout or connection refused. Fix it with a hosts file entry (below) or by enabling hairpin NAT on the router.
  • Stale hosts file entry. After a migration, /etc/hosts on the new server sometimes still maps the domain to the old server’s IP, often left over from testing before DNS switched. getent hosts reveals it.
  • DNS mismatch. The server uses a resolver that still has old records, or split DNS returns an internal address that is wrong.
  • Firewall. An outbound rule blocks connections to the server’s own public IP, or a tool such as fail2ban or CSF has banned the server’s own IP after it made many requests to itself. Check the firewall’s deny list for your server’s IP.
  • Cloudflare in the path. With the orange cloud on, the loopback leaves your server, goes to Cloudflare and comes back. Security features can challenge it (see Step 5), and it adds latency.

Point the domain at the server locally (VPS)

LOCAL_IP example.com www.example.com

This is a line you add to /etc/hosts on the web server, as root. LOCAL_IP is the address that worked with --resolve above. List every hostname the site uses. This makes the server’s own requests go straight to itself, skipping NAT, Cloudflare and outside DNS, while visitors are unaffected. Good: Step 2 now returns 200 and Site Health passes on a rerun. Remember the entry exists: if you move the site later, remove it, or it becomes the stale entry described above. On shared hosting you cannot edit this file; send your findings to your host.

Step 4: Let the server through basic auth on staging

Password-protecting a staging site with HTTP basic auth is a good habit, and the most common reason for a 401 loopback. WordPress forwards the credentials on its loopback only when PHP can see them, and on many PHP-FPM setups it cannot. The clean fix is to let the server’s own IP through without a password while everyone else still needs one.

Apache (.htaccess)

AuthType Basic
AuthName "Staging"
AuthUserFile HTPASSWD_PATH
<RequireAny>
  Require ip SERVER_IP
  Require valid-user
</RequireAny>

This goes in the .htaccess file in your site folder, replacing your existing basic auth block, above # BEGIN WordPress. HTPASSWD_PATH is the full path to your existing password file, and SERVER_IP is the IP the loopback connects from, usually the server’s own public IP (the remote_ip from Step 2, if there is no proxy in between). RequireAny lets a request in if either rule matches. Good: Step 2 returns 200 and a browser elsewhere still gets a password prompt.

Nginx

satisfy any;
allow SERVER_IP;
allow 127.0.0.1;
deny all;
auth_basic "Staging";
auth_basic_user_file HTPASSWD_PATH;

These lines go in the site’s server { } block, in a file you edit on the server. satisfy any means a request passes if it matches either the IP allow list or a valid password. Check with sudo nginx -t and apply with sudo systemctl reload nginx. If the site is behind Cloudflare, the loopback arrives from a Cloudflare IP, which is one more reason to add the hosts file entry from Step 3.

Password protection added by a plugin or a panel’s directory privacy feature behaves the same way; look for an IP allow option there.

Step 5: Clear WAF, security plugin and REST blocks

A 403, or a challenge page instead of a normal answer, means something decided the server’s own request looked suspicious. A POST to wp-cron.php with no referrer, from a data center IP, is exactly what many rules are built to stop.

  • ModSecurity. Look in your panel’s ModSecurity hits list or the audit log for entries with your server’s IP and wp-cron.php or /wp-json/. Note the rule ID and ask your host to exclude it for your server’s IP, or exclude it yourself on a VPS.
  • Security plugins. Plugins with firewall, rate limiting or country blocking features can block the server. Check their live traffic or blocked requests log, and add the server’s IP to their allow list. Options that disable the REST API for visitors who are not logged in should not affect this test, because it sends your cookies, but options that disable the REST API completely will fail it with a 401, 403 or 404.
  • Cloudflare. Check Security > Events for blocked or challenged requests from your server’s IP. Add an IP Access rule allowing it, or a custom rule with the Skip action for requests from that IP, or route around Cloudflare with the hosts file entry.
  • REST 404. If /wp-json/ itself returns 404, save Settings > Permalinks once to rebuild rewrite rules, and check that .htaccess (Apache) or try_files (Nginx) sends unknown paths to index.php. Our 404 on every page guide covers rewrite rules in detail.

Step 6: Rule out sessions, plugin conflicts and busy workers

PHP sessions

If Site Health also reports “An active PHP session was detected”, fix that first. A plugin that calls session_start() locks your session file for as long as your page is loading. The Site Health tests send your cookies, including the session cookie, so the loopback waits for a lock held by the very page that is waiting for the loopback. Nothing moves until the 10 second timeout, and you see cURL error 28. The fix belongs in the plugin, which should close its session as soon as it has read it; until then, deactivate it or replace it.

Plugin and theme conflicts

The Health Check & Troubleshooting plugin’s Troubleshooting Mode disables all plugins and switches to a default theme for your session only, while visitors keep seeing the normal site. Enable it, rerun the Site Health tests, then re-enable plugins one by one until the failure returns. On staging, wp plugin deactivate --all followed by reactivating in batches does the same job faster.

No free PHP worker

The loopback needs a second PHP worker while the first one waits for it. If every worker is busy, or the pool allows only one or two, the loopback queues until it times out. This shows up as intermittent cURL error 28 under load. Our 503 and PHP workers guide explains how to check and raise the worker limit.

What breaks while loopback is failing

  • The plugin and theme file editors refuse to save PHP changes and show “Unable to communicate back with site to check for fatal errors, so the PHP change was reverted. You will need to upload your PHP file change by some other means, such as by using SFTP.”
  • WP-Cron cannot spawn from page loads, so scheduled posts miss, update checks and automatic updates stall, and backup schedules stop. Our missed schedule guide shows how to replace it with a real cron job, which does not need a loopback at all.
  • Background processing in plugins that hand work off with a loopback request (imports, image optimization queues, some email queues) stalls or runs only when you are on the screen.
  • Site Health itself shows false alarms in other tests that depend on these requests.

Advanced: Trace the request in detail

When the cause is still unclear, watch the full exchange from the server with verbose curl:

curl -sv -o /dev/null -X POST https://example.com/wp-cron.php 2>&1 | grep -E "^\* (Trying|Connected|SSL|Server certificate)|^< HTTP|^< server|^< cf-ray|subject:|issuer:"

Run this over SSH on the web server, with your domain in place of example.com. -v prints the connection steps (lines starting with *) and the response headers (lines starting with <); 2>&1 sends them through the pipe, and grep -E keeps the interesting ones. Read it in order: “Trying” shows the IP chosen; “Connected” proves the TCP connection worked; the certificate subject and issuer show which certificate answered (a different site’s certificate means you hit the wrong virtual host or server); the HTTP line gives the status; server: cloudflare and a cf-ray header mean the request went out through Cloudflare. If it stops after “Trying” with no “Connected”, the problem is the network path (Step 3).

Next, check whether any code turned certificate verification on for local requests, which is the only way a self-signed or mismatched certificate breaks these tests:

wp eval 'var_dump( apply_filters( "https_local_ssl_verify", false, site_url() ) );'

Run this over SSH from the site folder. It prints bool(false) in a default install. bool(true) means a plugin, theme or must-use plugin has enabled verification, and the loopback now fails with cURL error 60 if the certificate is self-signed, expired or for another name. Either install a valid certificate (Let’s Encrypt works on staging subdomains too) or find and remove the filter with grep -rn "https_local_ssl_verify" wp-content. The OpenSSL and curl cheat sheets cover reading certificate chains in more depth.

Finally, to see what WordPress’s HTTP layer receives in full, extend the wp eval test from Step 2 to print the response headers with print_r( wp_remote_retrieve_headers( $r ) ). A header such as cf-mitigated: challenge, a www-authenticate header or a security plugin’s own header usually names the blocker outright.

When to contact your host

Open a ticket if the server cannot resolve or reach its own domain, if you cannot edit the hosts file, firewall or ModSecurity rules, or if loopbacks time out only under load. Send:

  • the domain and the exact Site Health detail line;
  • the output of the Step 2 curl commands and getent hosts, with the UTC time you ran them;
  • the result of the --resolve test;
  • any ModSecurity rule IDs, firewall entries or Cloudflare ray IDs you found;
  • whether the site uses basic auth, Cloudflare or a security plugin.

Our diagnostic tools show how your domain resolves and responds from the outside, which is a useful comparison with what the server sees from inside. If the site is down for visitors too, start at the Emergency Room.

Still stuck?

Print a free one-page checklist to keep, or open the Emergency Room if the symptom does not match.

Get the free checklists

Keep reading

All field guides

Search the field manual

What is going wrong?

Press / to search from any page, Esc to close