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) orfail(unhealthy). If any test isfail, the overall result isfailand 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-storeso 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.
- On the dashboard, click "+ Add New Monitor" and choose "HTTP(s)" as the monitor type
- Enter
https://example.com/wp-json/wpdr/v1/health?token=TOKENas the URL - 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
healthyas "present means up", it also matches a word that means failure, such asunhealthy. 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_TOKENif you suspect it has leaked. If your monitoring service lets you set request headers, sending the token in theX-Health-Tokenheader 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
healthyinsideunhealthy
Related articles:
- How to set up uptime monitoring for a WordPress site with UptimeRobot
- How to fix WP-Cron not running in WordPress
- How to speed up WordPress with a Redis object cache
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)