← Back to blog

WordPress Site Health: what the score is, and which tests lie to you

The SandyWP team 3 min read

Site Health lives at Tools > Site Health and runs about two dozen checks, then shows either "Good" or "Should be improved" next to a percentage circle. The percentage is not a quality rating: it is 100 - ceil((failedTests / totalTests) * 100) over a weighted count of the tests that happened to run.

Which tests run at all depends on WP_ENVIRONMENT_TYPE, a constant most sites never set. That one detail explains most of the complaints people cannot fix.

How the score is actually calculated

The maths is four lines of JavaScript in site-health.js, and it has nothing to do with speed, security posture, or anything a scanner would measure:

var totalTests  = good + recommended + ( critical * 1.5 );
var failedTests = ( recommended * 0.5 ) + ( critical * 1.5 );
var val = 100 - Math.ceil( ( failedTests / totalTests ) * 100 );

A recommended issue costs half a point. A critical issue costs one and a half, but it also adds one and a half to the denominator, so a critical failure on a site with twenty passing tests only moves the number by about seven points.

The label is a separate check, and it is stricter than the circle:

if ( 80 <= val && 0 === critical ) { // "Good"

So one critical issue means "Should be improved" no matter how high the percentage reads. That is why a site at 93% can still be orange, and it is the single most confusing thing about the page.

Three consequences worth knowing. The denominator is the number of tests that ran, so disabling tests raises your score without changing anything about the site.

A site with fewer passing tests is punished harder for the same single failure. And the number is not comparable between two sites, because they may not have run the same checks.

The real test list

Core registers twenty-one direct tests, which run in PHP during the page load, and four async tests, which the browser fires one at a time over the REST API. From class-wp-site-health.php on trunk, the direct ones are:

WordPress Version, Plugin Versions, Theme Versions, PHP Version, PHP Extensions, PHP Default Timezone, PHP Sessions, Database Server version, Secure communication, Scheduled events, HTTP Requests, REST API availability, Debugging enabled, File uploads, Plugin and theme auto-updates, Plugin and theme temporary backup directory access, Available disk space, Autoloaded options, Open Registration with privileged default role, Search Engine Visibility, Opcode cache.

The async ones are Communication with WordPress.org, Background updates, Loopback request, and HTTPS status.

Then three more are conditional, and this is where the count stops being fixed:

  • Authorization header is added only when wp_is_site_protected_by_basic_auth() is false. Put the site behind HTTP Basic Auth and the test disappears, because it would always fail.
  • Page cache and Persistent object cache are registered only when wp_get_environment_type() returns production.
  • HTTPS status is removed entirely when the environment type is development or local.

Why your sandbox complains about caching

wp_get_environment_type() returns production unless you tell it otherwise, via the WP_ENVIRONMENT_TYPE constant in wp-config.php or an environment variable of the same name. The accepted values are local, development, staging and production.

Nobody sets it. So a throwaway install that exists for twenty minutes to test one plugin gets told it has no page cache, no persistent object cache, and should really sort out its HTTPS.

Setting the constant removes all three complaints at once:

define( 'WP_ENVIRONMENT_TYPE', 'development' );

Note the asymmetry. staging still runs the cache tests, because only production is excluded from the "only check for caches in production environments" condition, while staging is not in the list that skips the HTTPS test either.

If you want the quiet version, development and local are the two values that actually turn things off.

This is worth doing on any disposable copy before you read the page, because otherwise three of your failures are about the environment rather than the thing you are investigating. On a SandyWP sandbox you have SSH and WP-CLI on the real install, so it is one wp config set WP_ENVIRONMENT_TYPE development away.

The failures that are not about your site

Four tests fail routinely on perfectly healthy installs.

Loopback request. WordPress asks its own URL over HTTP and checks it gets a response. It fails when the server cannot resolve its own hostname, when a /etc/hosts entry points the domain somewhere else, when a security plugin blocks or obscures wp-login.php, or when DNS has recently moved and has not settled.

The usual symptom is cURL error 28: Operation timed out, and the site itself works fine throughout.

Background updates. This one checks whether core could update itself unattended, and it reports a problem when the filesystem is not directly writable, when AUTOMATIC_UPDATER_DISABLED is set, or when the install is under version control. All three are deliberate choices on a managed host, and the core docblock says as much: automatic update checks "may be handled by a host, and are therefore disabled in core".

Debugging enabled. It warns whenever WP_DEBUG is on. That is correct advice for production and actively unhelpful on a copy you are debugging, which is the whole reason debug mode exists as a per-sandbox toggle rather than a global setting.

Search Engine Visibility. It flags the "Discourage search engines" checkbox, which is exactly what you want set on a staging copy or a client demo.

The honest reading: Site Health is written for a single production site, so on any non-production install several of its recommendations are instructions to make the install less suitable for its purpose. Read the Status list, not the colour.

Why the red badge in the admin menu goes stale

The number next to "Tools" in the admin menu is not calculated live. It comes from a transient:

$issue_counts = get_transient( 'health-check-site-status-result' );

That transient is written with set_transient() and no expiry argument, so it never expires on its own. It is only overwritten when someone loads the Status tab, or when the wp_site_health_scheduled_check cron event runs.

That event is scheduled weekly, starting a day after it is first registered. Weekly, and it depends on WP-Cron firing, which on a site nobody visits means it does not fire at all.

So the badge can be a week old, or arbitrarily old on a quiet site, and it can show a count for an issue you fixed days ago. Load Tools > Site Health to force a recount.

The cron run also skips two tests on purpose: rest_availability and authorization_header both carry skip_cron => true, because they need a real browser request to be meaningful.

The Info tab is the more useful half

The Status tab is the one everyone looks at. The Info tab is the one worth copying.

It lists your WordPress, PHP and database versions, the active theme and plugin versions, server limits (memory_limit, max_execution_time, upload_max_filesize), the uploads directory path and writability, constants in force, and whether cURL and the required PHP extensions are present. There is a Copy site info to clipboard button at the top, and it produces a plain text block.

That block is the single best thing to attach to a bug report. It also tells you what to match when you build a copy to reproduce something on, which is the practical use of the page: Site Health tells you what the environment is, and a sandbox with the same PHP and WordPress versions tells you whether the environment is the cause.

What Site Health will not tell you

It is a configuration checker, not a diagnostic. It does not look at your content, your queries, your error log, or your actual page speed.

Specifically, it will not find a plugin conflict, a slow query, a broken cron callback, or a fatal error on one template. A green "Good" on a site that white-screens on the checkout page is not a contradiction, because none of the tests load the checkout page.

When the Status list is clean and something is still wrong, the next step is reproduction rather than more scanning. Copy the site, turn on debug mode, and walk the broken flow with the log open, which is the approach in the white screen debugging guide and the plugin conflict guide.

The short version

Set WP_ENVIRONMENT_TYPE on anything that is not production, and three of your failures vanish because they were never relevant.

Ignore the percentage. Read the critical items, check whether each one is about your site or about your server, and treat the Info tab as the deliverable.

And if the badge count disagrees with the page, trust the page. The badge is a cached number from a weekly cron job that may never have run.