2026年11月12日

2026年11月12日

WordPress Health Check Endpoint: Uptime Monitoring and Slack Alerts

This article is a localized edition of the Japanese original. Read the Japanese original

Even when your home page returns 200, parts of the site can be broken: the uploads folder may not be writable, or the object cache (Redis and so on) may not respond. In this article you add a "health check endpoint" to the WordPress REST API that tests the main components, so that both an external monitoring service and WordPress's own scheduled tasks can tell you when something is wrong.

The code was tested on WordPress 7.1.3 with PHP 8.3 in a test environment as of October 2026.

How the pieces fit together

Component Role
/wp-json/wpdr/v1/health Tests the database, uploads folder and object cache; returns 200 if everything is fine and 503 if anything fails
Token Only requests that match the string written in wp-config.php get a response
External monitoring (UptimeRobot, etc.) Calls the endpoint every few minutes and alerts you by email or other channels on anything other than 200
WP-Cron self-check Runs the same tests every 5 minutes and notifies Slack or email when the site becomes unhealthy and when it recovers

When WordPress cannot start at all (a PHP error, no database connection and so on), WP-Cron does not run either. So treat external monitoring as the main alerting path and the WP-Cron self-check as a backup. If WordPress cannot connect to the database, it returns an error page (status 500) before it ever reaches the endpoint code, so external monitoring still catches it.

Step 1: Add the endpoint code

Save the following code as wp-content/mu-plugins/wpdr-health-check.php (create the mu-plugins directory if it does not exist). Files in mu-plugins are always loaded, with no activation step.

<?php
/**
 * Plugin Name: WPDR Health Check
 * Description: Tests the database, uploads folder and object cache at /wp-json/wpdr/v1/health and returns 503 on failure. Runs the same tests from WP-Cron and notifies on failure and recovery.
 */

/**
 * Runs each test and returns the results.
 *
 * @return array{status: string, checks: array<string, array{status: string, message: string}>}
 */
function wpdr_health_run_checks(): array {
	global $wpdb;
	$checks = array();

	// 1. Database
	$db_ok                = ( '1' === (string) $wpdb->get_var( 'SELECT 1' ) );
	$checks['database'] = array(
		'status'  => $db_ok ? 'pass' : 'fail',
		'message' => $db_ok ? 'connected' : 'query failed',
	);

	// 2. Is the uploads folder writable? (create and delete a temporary file)
	$upload    = wp_upload_dir( null, false );
	$test_file = trailingslashit( $upload['basedir'] ) . '.wpdr-health-' . wp_generate_password( 8, false );
	$fs_ok     = ( false !== @file_put_contents( $test_file, 'ok' ) ); // phpcs:ignore WordPress.PHP.NoSilencedErrors
	if ( $fs_ok ) {
		wp_delete_file( $test_file );
	}
	$checks['uploads'] = array(
		'status'  => $fs_ok ? 'pass' : 'fail',
		'message' => $fs_ok ? 'writable' : 'not writable',
	);

	// 3. Only with a persistent object cache (Redis, Memcached, etc.): write a value and read it back
	if ( wp_using_ext_object_cache() ) {
		$value    = wp_generate_password( 12, false );
		$cache_ok = wp_cache_set( 'probe', $value, 'wpdr_health', 60 ) && wp_cache_get( 'probe', 'wpdr_health' ) === $value;
		$checks['object_cache'] = array(
			'status'  => $cache_ok ? 'pass' : 'fail',
			'message' => $cache_ok ? 'read/write ok' : 'read/write failed',
		);
	}

	$status = 'pass';
	foreach ( $checks as $check ) {
		if ( 'fail' === $check['status'] ) {
			$status = 'fail';
			break;
		}
	}

	return array(
		'status' => $status,
		'checks' => $checks,
	);
}

/**
 * Registers the REST API endpoint /wp-json/wpdr/v1/health.
 */
add_action(
	'rest_api_init',
	function (): void {
		register_rest_route(
			'wpdr/v1',
			'/health',
			array(
				'methods'             => 'GET',
				'callback'            => 'wpdr_health_rest_response',
				'permission_callback' => 'wpdr_health_rest_permission',
			)
		);
	}
);

/**
 * Allows the request only when the token matches WPDR_HEALTH_TOKEN in wp-config.php.
 * The token is accepted in the X-Health-Token header or as ?token=.
 */
function wpdr_health_rest_permission( WP_REST_Request $request ): bool {
	$expected = defined( 'WPDR_HEALTH_TOKEN' ) ? (string) WPDR_HEALTH_TOKEN : '';
	if ( strlen( $expected ) < 32 ) {
		return false; // If the token is missing or too short, respond to no one
	}
	$given = $request->get_header( 'x_health_token' );
	if ( null === $given ) {
		$given = $request->get_param( 'token' );
	}
	return is_string( $given ) && hash_equals( $expected, $given );
}

function wpdr_health_rest_response(): WP_REST_Response {
	$result   = wpdr_health_run_checks();
	$response = new WP_REST_Response(
		array(
			'status'    => $result['status'],
			'checks'    => $result['checks'],
			'timestamp' => gmdate( 'c' ),
		),
		'pass' === $result['status'] ? 200 : 503
	);
	$response->header( 'Cache-Control', 'no-store' );
	return $response;
}

/**
 * Runs the same tests from WP-Cron every 5 minutes and notifies only when the state changes.
 */
add_filter(
	'cron_schedules',
	function ( array $schedules ): array {
		$schedules['wpdr_five_minutes'] = array(
			'interval' => 5 * MINUTE_IN_SECONDS,
			'display'  => 'Every 5 minutes (WPDR health)',
		);
		return $schedules;
	}
);

add_action(
	'init',
	function (): void {
		if ( ! wp_next_scheduled( 'wpdr_health_self_check' ) ) {
			wp_schedule_event( time(), 'wpdr_five_minutes', 'wpdr_health_self_check' );
		}
	}
);

add_action(
	'wpdr_health_self_check',
	function (): void {
		$result   = wpdr_health_run_checks();
		$was_fail = (bool) get_option( 'wpdr_health_failing', false );

		if ( 'fail' === $result['status'] && ! $was_fail ) {
			$failed = array_keys( wp_list_filter( $result['checks'], array( 'status' => 'fail' ) ) );
			wpdr_health_notify( 'Problem detected: ' . implode( ', ', $failed ) );
			update_option( 'wpdr_health_failing', 1, false );
		} elseif ( 'pass' === $result['status'] && $was_fail ) {
			wpdr_health_notify( 'Recovered' );
			delete_option( 'wpdr_health_failing' );
		}
	}
);

/**
 * Sends to a Slack Incoming Webhook. If no URL is set, sends to the admin email.
 */
function wpdr_health_notify( string $message ): void {
	$text = sprintf( '[%s] %s (%s)', home_url(), $message, wp_date( 'Y-m-d H:i:s' ) );

	if ( defined( 'WPDR_SLACK_WEBHOOK_URL' ) && WPDR_SLACK_WEBHOOK_URL ) {
		wp_remote_post(
			WPDR_SLACK_WEBHOOK_URL,
			array(
				'headers' => array( 'Content-Type' => 'application/json' ),
				'body'    => wp_json_encode( array( 'text' => $text ) ),
				'timeout' => 5,
			)
		);
		return;
	}

	wp_mail( get_option( 'admin_email' ), 'WordPress health check', $text );
}

Key points of the code:

  • Each test reports pass (healthy) or fail (unhealthy). If any test is fail, the overall result is fail and the status is 503
  • The object cache test only runs when a persistent object cache such as Redis Object Cache is in use. On sites without one, the item does not appear at all
  • The response carries Cache-Control: no-store so page caches and CDNs do not store the result
  • Notifications are sent only when the state changes from healthy to unhealthy or back. You do not get a notification every 5 minutes while a problem continues

Step 2: Add the token and notification target to wp-config.php

Make the token a long string that cannot be guessed. Running the following command on the server gives you a random 64-character string.

openssl rand -hex 32

Add the following to wp-config.php above the line require_once ABSPATH . 'wp-settings.php';.

// Health check token (replace with a value made by openssl rand -hex 32)
define( 'WPDR_HEALTH_TOKEN', 'your-64-character-random-string' );

// Slack Incoming Webhook URL (leave this line out if you do not use Slack; alerts then go to the admin email)
define( 'WPDR_SLACK_WEBHOOK_URL', 'https://hooks.slack.com/services/XXXX/XXXX/XXXX' );

A Slack Incoming Webhook URL is issued when you enable Incoming Webhooks in your Slack app's settings and choose the channel to post to. Anyone who knows the URL can post to that channel, so keep it private.

Step 3: Check the endpoint’s response

Call it with curl and check the status code and response (replace TOKEN with the value from Step 2).

# Pass the token in a header
curl -s -w '\n%{http_code}\n' -H 'X-Health-Token: TOKEN' https://example.com/wp-json/wpdr/v1/health

# Without a token (401 means the token check is working)
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/wp-json/wpdr/v1/health

When everything is healthy, you get a response like this with status 200.

{"status":"pass","checks":{"database":{"status":"pass","message":"connected"},"uploads":{"status":"pass","message":"writable"}},"timestamp":"2026-10-29T16:06:29+00:00"}

If the uploads folder is not writable, uploads becomes fail and the status is 503.

If your permalink setting is "Plain" (URLs like ?p=123), /wp-json/ URLs do not work. Use https://example.com/?rest_route=/wpdr/v1/health instead.

Step 4: Monitor from outside with UptimeRobot

In UptimeRobot, create a monitor that calls the endpoint on a schedule. The steps follow UptimeRobot's help article.

  1. On the dashboard, click "+ Add New Monitor" and choose "HTTP(s)" as the monitor type
  2. Enter https://example.com/wp-json/wpdr/v1/health?token=TOKEN as the URL
  3. Choose the alert contacts and the monitoring interval, then click "Create monitor"

UptimeRobot's HTTP(s) monitor decides up or down from the response status code. This endpoint returns 503 on failure, so the status code alone is enough to detect problems.

  • If you use a "Keyword" monitor, which judges by a string in the response body, be careful with the word you pick. For example, if you treat healthy as "present means up", it also matches a word that means failure, such as unhealthy. The code in this article reports the result through the status code, so an HTTP(s) monitor is enough
  • With a wrong token, the endpoint returns status 401. UptimeRobot may treat 401 specially, so before registering, confirm with Step 3 that you get 200, and make sure the token in the URL is typed correctly
  • A token in the URL ends up in web server access logs and in the monitoring service's settings. Use a value dedicated to monitoring, and change WPDR_HEALTH_TOKEN if you suspect it has leaked. If your monitoring service lets you set request headers, sending the token in the X-Health-Token header keeps it out of the URL

As of October 2026, UptimeRobot's help center lists a 5-minute monitoring interval for the free plan.

Step 5: Check the WP-Cron self-check

Once the code is in place, a 5-minute schedule is registered. You can confirm it and run it manually with WP-CLI.

# Is the schedule registered?
wp cron event list --fields=hook,next_run_relative,recurrence | grep wpdr

# Run it now (a notification is sent only when the state changes)
wp cron event run wpdr_health_self_check

WP-Cron runs when someone visits the site. On low-traffic sites it may not run on schedule, so in production we recommend calling WP-Cron from the server's cron (Optimizing WordPress WP-Cron for production).

Caveats

  • Plugins that restrict the REST API: if a security plugin blocks REST API access for logged-out users, the endpoint returns 401 or 403 and the monitor always reports the site as down. In that case, add a setting that allows /wp-json/wpdr/v1/health
  • Page caches and CDNs: the response sends Cache-Control: no-store, but if your CDN settings force caching, exclude this URL from caching
  • Load from the checks: each call runs one database query and creates and deletes one small file. There is no need to call it more often than once a minute
  • Adding checks: if the site depends on other functions such as sending email or external APIs, you can add items to wpdr_health_run_checks() in the same way. Keep in mind that a check that talks to an external service marks your site as unhealthy when that service is down

Summary

  • Add a REST API endpoint that tests the main components and returns 200 or 503, protected by a token
  • Use external monitoring (an UptimeRobot HTTP(s) monitor) to detect failures by status code
  • Use the WP-Cron self-check as a backup that notifies Slack or email only when the state changes
  • If you match on a string, avoid words contained in other values, such as healthy inside unhealthy

Related articles:

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲