Drupal: a healthcheck that checks every service your site depends on

Cet article est aussi disponible en français

Monitoring a Drupal site with Uptime Kuma (or any other probe) often comes down to checking that the homepage returns a 200. The problem is that the homepage returns a 200 in plenty of situations where the site is in bad shape: the page is served from cache while the database is down, Redis has stopped answering but the page cache is still holding up, Solr is down and search returns an empty page, the disk is full and no upload goes through anymore… You find out from a user, not from the probe.

So I wrote a small module, kgaut/healthcheck, which exposes a /health route that actually queries the services the site depends on, and returns a JSON verdict that a probe or a CI job can read.

$ curl -sS https://www.example.com/health
{"status":"ok","checks":{"database":"ok","redis":"ok","search_api":"warning","files":"ok"}}

Installation

composer require kgaut/healthcheck
drush en healthcheck

No configuration is needed. The module requires Drupal 11.1 or later and PHP 8.3.

What gets checked

CheckWhat it doesCritical
databasea SELECT 1 on the default connectionyes
redisa PING through the redis module, if a connection is declaredyes
search_apithe availability of each active Search API server (a ping for Solr)no
fileswrites, reads back, then deletes a file in public://.healthcheckyes

Each check returns one of four verdicts:

  • ok: the service answered;
  • failed: a critical check failed, and the response switches to 503;
  • warning: a non-critical check failed, and the response stays at 200 (a Solr outage degrades search, it doesn't make the site unavailable);
  • not_configured: the site doesn't use this service (no Redis, no Search API server). This is never a failure.

The response body contains no hostname, no path and no error message: the cause of a failure goes to the Drupal log (healthcheck channel). The verdict is kept for 30 seconds, so a burst of requests doesn't hammer the database or Redis, but the HTTP response itself is never cached (Cache-Control: no-store).

One rule for the probe

If you only remember one thing: a probe should only consider the site healthy on an HTTP 200 whose body contains "status":"ok", without following redirects.

Why so much caution? Because a database that can't be reached when Drupal boots gives a 500 (not a 503 from the module, Drupal doesn't even start), because a proxy or a maintenance page can answer 200 with its own content, and because a misconfigured site can redirect /health to the installer or to /fr/health. In Uptime Kuma, that's an "HTTP(s) - Keyword" monitor on "status":"ok", with redirects disabled.

Per-site and per-environment settings

Everything lives in the healthcheck.settings config, exported along with the rest of the site's configuration. You can, for instance, make Search API critical on a site where search is at the heart of the business, or disable a check. And like any config, it can be overridden in settings.php:

// Search API critical in production only.
$config['healthcheck.settings']['checks']['search_api']['critical'] = TRUE;
// The write test in another directory.
$config['healthcheck.settings']['files_directory'] = 'private://healthcheck';

Adding your own checks

Checks are plugins. A site that depends on a third-party API, a mail server or any other service can add its own from any module, with a PHP attribute:

namespace Drupal\my_module\Plugin\HealthCheck;

use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\healthcheck\Attribute\HealthCheck;
use Drupal\healthcheck\CheckResult;
use Drupal\healthcheck\HealthCheckBase;

#[HealthCheck(id: 'my_service', label: new TranslatableMarkup('My service'), critical: TRUE, weight: 40)]
final class MyService extends HealthCheckBase {

  public function check(): CheckResult {
    // Actually query the service, with a short timeout.
    return CheckResult::ok();
  }

}

With drush

drush healthcheck:run                 # table: check, verdict, critical, cause
drush healthcheck:run --format=json   # for a CI

The command checks on every call (without the 30-second cached verdict), shows the cause of each failure, and exits with an error if a critical check fails. Handy at the end of a deployment script, or to understand why the probe is red without digging through the logs.

Post-deployment check in CI

This is the use case that made me write the module: on my uzinasit infrastructure, each tag deploys the sites, then a CI job checks that each one answers properly on its health endpoint. The module ships the script for that, scripts/ci-health.sh, which only needs bash and curl:

prod:health:
  stage: postdeploy
  image: wodby/drupal-php:8.3-dev     # any image with bash and curl
  needs:
    - job: prod:clear-cache
      artifacts: false
  variables:
    HEALTH_URL: https://www.example.com/health
  script:
    - bash web/modules/contrib/healthcheck/scripts/ci-health.sh "$HEALTH_URL"
  rules:
    - if: $CI_COMMIT_TAG

The script makes up to 6 attempts, 10 seconds apart (time for the caches to warm up after being cleared), applies the "200 + "status":"ok" without redirects" rule, and shows the status code and body of each attempt when it fails. It doesn't block anything, the deployment is already done, but the pipeline turns red, and you know right away.

Where the module stands

The module is in beta (1.0.0-beta5 at the time of writing), it runs on my sites and 1.0.0 is getting close. The code and the full documentation (operations, writing a check, design decisions) are on GitHub (in French for now), and the package is on Packagist. Feedback is welcome in the issues.

Written by Kevin Gautreau — freelance PHP/Drupal developer & trainer in Auvergne, France. get in touch

Related content

article · 29 May 2026 Uzinasit: lessons learned from my Drupal 11 site factory I took advantage of having a bit of free time during summer 2025 to start rationalizing all my websites, to make them easier to manage Why a site factory?… article · 13 Oct 2026 Drupal: raw_fields_formatters, raw data for SDC components and decoupled front ends I've been working on decoupled Drupal projects for a few years now (Drupal as the back end for content editing, with an Angular front end or a mobile app, for… article · 19 Oct 2026 Drupal 11: a reusable paragraphs package with their SDC components Every Drupal site I maintain in my uzinasit ends up needing the same content blocks: rich text, text with an image, a gallery, a video, a map, a button, a…

Comments (0)