Self-hosted analytics has a failure mode nothing else does: the product's own output hides the outage. When a Plausible or Umami server stops ingesting, the graph does not go red. It goes to zero, which looks exactly like a slow Tuesday, and the person who notices is usually whoever opens the dashboard a week later to prepare a report. Meanwhile the tracking script may have been returning a 502 on every page of every site you track, invisibly, because a failed analytics beacon never shows a visitor anything.

This guide sets up external monitoring for the three self-hosted options people actually run: Plausible Community Edition, Umami, and, briefly, Matomo. Status checks and certificate monitoring are on every CronAlert plan; keyword assertions and heartbeats are Pro ($5/month) and are marked. The examples use stats.example.com as the public hostname.

What each server exposes

Plausible CEUmamiMatomo
ReadinessGET /api/health → 200 {"postgres":"ok","clickhouse":"ok","sites_cache":"ok","sessions":"ok"}, 500 if any is not oknone (see login page)none
LivenessGET /api/system/health/live → {"ok":true}GET /api/heartbeat → {"ok":true}none
VersionGET /api/system → build with version and commitin the UIin the UI
Tracking script/js/script.js (and variants)/script.js with data-website-id/matomo.js
IngestPOST /api/event, always 202; dropped events get x-plausible-dropped: 1POST /api/send, needs a proper User-Agent, returns sessionIdGET/POST /matomo.php?idsite=…&rec=1, returns a GIF (204 with send_image=0)
StoragePostgreSQL + ClickHousePostgreSQLMySQL/MariaDB
Default port80/443 via compose (HTTP_PORT)300080 behind your web server

All the health routes are unauthenticated. The ingest routes are the ones to keep monitors away from, for two reasons: HTTP monitors cannot send a request body, and anything that reaches them is counted.

Step 1: the readiness or liveness monitor

Plausible. Add an HTTP monitor on https://stats.example.com/api/health, expected status 200, timeout 15 seconds (the readiness handler waits up to 15 seconds for each database before calling it an error, so a shorter monitor timeout would fire first). This is a real readiness check: in the source, it issues SELECT 1 to both PostgreSQL and ClickHouse, checks that the site and shield caches are loaded, and checks that session transfer has been attempted, returning 500 unless all four are ok. The router comment marks /api/health as a legacy alias for /api/system/health/ready; monitor the new path if you are on a recent release, the old one still works today. On Pro, add the assertion contains "clickhouse":"ok" so you can tell a ClickHouse problem from a Postgres one by which monitor's body changes; or keep one monitor and read the body in the check result when it fails, since the JSON names the failing component.

Umami. Add a monitor on https://stats.example.com/api/heartbeat, expected 200. The handler returns {"ok":true} and nothing else, so this proves the Node process and your proxy, not the database. Pair it on Pro with a monitor on https://stats.example.com/login asserting contains umami (the page HTML references the app name and its assets); a login page that renders means the application layer is up, and a 500 there usually means DATABASE_URL is unreachable.

Matomo. There is no health route. Monitor https://stats.example.com/ (the login page) with expected 200 and, on Pro, contains Matomo. Matomo returns a styled error page when MySQL is down, and the keyword catches it where the status alone might not.

Intervals come from your plan (3 minutes on Free, 1 minute on Pro); alerts go to your team's channels. Why a status-only check on a web app is rarely enough explains the reasoning behind the keyword pairings.

Step 2: the tracking script (Pro for the keyword)

This is the monitor that catches the outage visitors cause you to miss. Every tracked page loads the script from your host: https://stats.example.com/js/script.js for Plausible (plus the extension variants such as script.outbound-links.js if your snippet uses one), https://stats.example.com/script.js for Umami, https://stats.example.com/matomo.js for Matomo. Add a monitor on the exact URL your snippet uses, expected 200, and on Pro assert contains a short string from the file (plausible, umami, or Matomo respectively, checked once with curl). It fails if the proxy stops routing the path, if a content blocker on your own CDN strips it, if the server is up but static assets failed to build after an upgrade, or if the certificate on the stats host breaks, which brings us to the next step.

Also watch the response time in the check history. The script is on every page's critical path for analytics; a stats host that takes four seconds to serve a 2 KB file is losing you the visitors who leave before it loads. The timeout is the only slowness alarm an HTTP monitor has, so set it tighter on this monitor than on the others, around 5 seconds.

Step 3: the certificate

The analytics host is loaded cross-origin by every site you track. An expired certificate there does not break those sites, which is the problem: the browser silently refuses the script and the beacon, and your traffic goes to zero with no error anywhere you look. SSL monitoring is included with every HTTP monitor on every plan, so the monitors above already alert before the certificate on stats.example.com expires. If Plausible's compose stack terminates TLS itself (it can, via HTTPS_PORT and its built-in ACME), renewal monitoring is still worth reading for what to do when the renewal job is the thing that breaks.

Step 4: an ingest canary (Pro)

Health says the databases answer. The script says the browser can load the code. Neither says an event posted by a visitor is being recorded, and the Plausible Events API documentation is explicit about why you cannot test that with a status code: the API always returns HTTP 202 Accepted, even when an event is dropped; a dropped event is marked only with the response header x-plausible-dropped: 1. HTTP monitors send no body and cannot assert on response headers, so this is a job for a script and a heartbeat:

# every 10 minutes: post a pageview for a dedicated test site; ping only if it was not dropped
*/10 * * * * curl -sS -D - -o /dev/null --max-time 10 -X POST https://stats.example.com/api/event \
    -H "User-Agent: Mozilla/5.0 (canary) CronAlert-ingest-check" \
    -H "Content-Type: application/json" \
    -d '{"domain":"canary.example.com","name":"pageview","url":"https://canary.example.com/ping"}' \
  | grep -qi '^HTTP/.* 202' && ! curl -sS -D - -o /dev/null --max-time 10 -X POST https://stats.example.com/api/event \
    -H "User-Agent: Mozilla/5.0 (canary) CronAlert-ingest-check" -H "Content-Type: application/json" \
    -d '{"domain":"canary.example.com","name":"pageview","url":"https://canary.example.com/ping"}' \
  | grep -qi '^x-plausible-dropped' \
  && curl -fsS --max-time 10 --retry 3 https://cronalert.com/api/heartbeat/YOUR-TOKEN

That is two requests to keep the shell logic readable; a five-line script that saves the headers to a file and tests both conditions on one request is cleaner. Create a site called canary.example.com in Plausible that nobody reads, so the test pageviews land somewhere harmless. The User-Agent header is required by the Events API. For Umami, post to /api/send with {"type":"event","payload":{"website":"<test website id>","hostname":"canary.example.com","url":"/ping","title":"canary"}} and a browser-like User-Agent (the documentation warns that without a proper one the request is not registered), and gate the ping on the response containing "sessionId". For Matomo, request matomo.php?idsite=<test site>&rec=1&send_image=0&url=https://canary.example.com/ping and gate on a 204.

Heartbeat expected interval 10 minutes, grace 10 to 15. Now a stuck ClickHouse insert queue, a Plausible that is dropping events for a site it does not recognise, or an Umami whose database is read-only shows up within a quarter of an hour instead of at month end. Grace periods explains the sizing.

Step 5: backups (Pro)

Plausible keeps two databases: PostgreSQL for sites, users, and goals, and ClickHouse for the events themselves, and losing the second one loses your history. The compose file's own healthchecks (pg_isready and ClickHouse's /ping) are for Docker, not for you. Back both up and make the job report:

# /etc/cron.daily/plausible-backup
set -euo pipefail
cd /opt/plausible
DEST=/srv/backups/plausible/$(date +%F); mkdir -p "$DEST"
docker compose exec -T plausible_db pg_dump -U postgres plausible_db | gzip > "$DEST/postgres.sql.gz"
docker compose exec -T plausible_events_db clickhouse-client --query "BACKUP DATABASE plausible_events_db TO Disk('backups', '$(date +%F)')" > "$DEST/clickhouse-backup.log"
# ship $DEST and the ClickHouse backup disk off the host ...
curl -fsS --max-time 10 --retry 3 https://cronalert.com/api/heartbeat/YOUR-TOKEN

The ClickHouse line assumes a backups disk is configured; adapt it to however you back ClickHouse up (some people snapshot the volume instead). Umami is one pg_dump; Matomo one mysqldump plus its config/config.ini.php. Heartbeat expected interval 1 day, grace a few hours; set -e aborts before the ping if any step fails. Monitoring database backups adds the restore test.

What not to monitor

  • The ingest endpoints with an HTTP monitor. You cannot send a body, and for Matomo a GET to matomo.php?idsite=1&rec=1 works, which is worse: every check becomes a recorded visit. Ingest testing belongs in the heartbeat script with a test site.
  • The stats dashboard for a real site. It needs a session (or a shared-link token), and a monitor holding one is a monitor that can read your traffic data. The login page covers the application layer.
  • Your tracked sites' traffic level. Plausible and Umami have traffic-spike notifications of their own, and some people want a "traffic dropped to zero" alarm. That is a metric alert, not an uptime check; the canary above is the uptime version of the same question.
  • Plausible's internal services separately. The readiness endpoint already aggregates Postgres, ClickHouse, and the caches; monitoring ClickHouse's /ping directly from outside would mean exposing port 8123, which you should not.

Reading an alert

AlertLikely causeFirst thing to check
Health or heartbeat endpoint timeoutHost, container, or proxy downdocker compose ps; proxy logs
Plausible /api/health 500One of postgres, clickhouse, sites_cache, sessions not okThe JSON body in the check result names it; ClickHouse disk space is the usual culprit
Umami heartbeat green, login keyword redDatabase unreachable or migration pendingdocker compose logs umami; DATABASE_URL
Script monitor red, health greenProxy path rule, static assets, or CDN rulecurl -I the script URL; proxy config
Script monitor slowOverloaded host or proxy bufferingResponse time history; host load
Ingest heartbeat overdue, everything else greenEvents being dropped (unknown site, rate limit, queue stuck)Run the canary by hand with -D - and read the headers
Certificate alertRenewal stoppedProxy or ACME logs
Backup heartbeat overdueCron not running, dump failed, disk fullRun the script by hand

Frequently asked questions

Which Plausible health path should I use?

Both /api/health and /api/system/health/ready run the same readiness handler today; the router comments mark the short one as the legacy alias. Use the /api/system/health/ paths on new installs. /live is the right target for a load balancer; /ready is the right target for an uptime monitor because it includes the databases.

Does checking /api/health every minute load the databases?

Two SELECT 1 queries a minute is nothing next to event ingest. Plausible's own cloud documentation points external checkers at the same endpoint.

Can I monitor Plausible Cloud or Umami Cloud this way?

The script monitor and the ingest canary work against the hosted services and are useful: they tell you when the vendor is down or when your account has hit a limit and is dropping events. Health endpoints and backups are the vendor's concern. Third-party dependency monitoring covers the general pattern.

Which parts are free?

The health, heartbeat, login-page, and script monitors as status checks, plus certificate monitoring, are free. The keyword assertions and the two heartbeats (ingest canary, backup) need Pro at $5/month.

Get started

Two free monitors, one on the health endpoint and one on the tracking script, turn "the graph looks quiet" back into "the server is down". Create a free CronAlert account and add them; the ingest canary comes next. Related reading: monitoring Docker self-hosted services, monitoring CDN and static assets, monitoring behind a reverse proxy, monitoring the VPS underneath, and uptime monitoring vs APM vs logging.