A Matrix homeserver fails in layers, and the layer your users see is the last to go. Element keeps showing cached rooms when Synapse is unreachable; local users keep chatting while federation to the rest of the network is broken; a wrong .well-known file stops every other server from delivering to you while your own clients never notice. The admin finds out from a friend on another server asking why nobody has answered in two days. This guide sets up external monitoring that catches each layer, using what Synapse and the Matrix specification already expose without authentication.
The examples assume Synapse behind a reverse proxy with the homeserver name example.com and the actual server at matrix.example.com, which is the common delegated setup. Conduit, conduwuit, and Dendrite implement the same specification endpoints, so everything except the Synapse-specific /health and admin paths applies to them too. Status checks are free on CronAlert; keyword assertions and heartbeats are Pro ($5/month) and are marked.
What a homeserver exposes to check
| URL | Auth | What 200 proves | Touches DB? |
|---|---|---|---|
/health | none | Synapse process up and listening (Synapse only) | no |
/_matrix/client/versions | none | Client API routed; JSON lists supported spec versions | no |
/_matrix/client/v3/login | none | Login flows configured (m.login.password, SSO) | no |
/_matrix/federation/v1/version | none | Federation API reachable on the federation port | no |
/.well-known/matrix/server | none | Delegation file served with the right m.server | no |
/.well-known/matrix/client | none | Client discovery points at the right base_url | no |
/_matrix/client/v3/profile/@user:example.com | none* | A real row came back from PostgreSQL | yes |
/_matrix/key/v2/server | none | Signing key served (other servers fetch this) | no |
* unless limit_profile_requests_to_users_who_share_rooms is enabled. Two things not in the table are deliberate: the admin API under /_synapse/admin needs an admin access token and does not belong in a monitor, and the metrics listener at /_synapse/metrics is for Prometheus and should not face the internet at all.
Step 1: /health through the proxy
Synapse's reverse proxy documentation says it plainly: each configured HTTP listener has a /health endpoint which always returns 200 OK and does not get logged. It is on by default, so you do not need to add the health resource to your listener unless you want it on a separate port. Create an HTTP monitor on https://matrix.example.com/health, method GET, expected status 200, timeout 10 seconds. Because it goes through the public hostname, a green check means DNS, TLS, the reverse proxy, and the Synapse process are all working.
Be clear about the limit: the handler returns a constant. PostgreSQL can be down, the disk can be full, and /health stays 200 until the process itself stops. That is why it is step 1 and not the whole guide. If you run Synapse with workers, /health on the public hostname tells you about whichever process the proxy routed to, usually the main process or a generic worker; it says nothing about the others.
The check interval is set by your plan, 3 minutes on Free and 1 minute on Pro, and alerts go to your team's channels: email, push, Slack, Discord, Telegram, or a webhook. A Matrix admin usually wants the alert somewhere that is not Matrix.
Step 2: the client API and the well-known file users depend on
Add a monitor on https://matrix.example.com/_matrix/client/versions. Authentication is optional for this endpoint; without it, the server returns the versions available to everyone, as JSON with a versions array. On Pro, add the assertion contains "versions"; on Free, the 200 alone tells you the client API path is routed through the proxy, which is a different failure from /health because proxies are usually configured per path prefix.
Then monitor the client discovery file at https://example.com/.well-known/matrix/client, on the homeserver name host, not the server host. This is what Element reads when someone types example.com on the login screen. It should return JSON like {"m.homeserver":{"base_url":"https://matrix.example.com"}}. On Pro, assert contains "base_url":"https://matrix.example.com", exactly as your web server emits it (check the spacing with curl first). A website redesign that drops this file does not break anyone already logged in, which is exactly why you need a monitor to notice it.
Step 3: federation, on the port other servers actually use
Federation breaks separately from the client API and more quietly. The specification's resolution rules say a remote server first fetches https://example.com/.well-known/matrix/server; if that returns an m.server value such as matrix.example.com:443, it talks to that host and port; if there is no well-known and no SRV record, it falls back to example.com on port 8448. So the federation endpoint you must monitor is wherever that chain ends up, and the well-known file is part of the surface.
- The delegation file. Monitor
https://example.com/.well-known/matrix/serverwith, on Pro, the assertion contains"m.server":"matrix.example.com:443"(or whatever you delegate to). Remote servers cache the result, the spec's recommended default being 24 hours, so a broken file can take a day to bite and a day to heal; the monitor tells you the moment it changes. If you setserve_server_wellknown: trueso Synapse serves this file itself, the monitor also covers the proxy rule that forwards it. - The federation version endpoint. Monitor
https://matrix.example.com/_matrix/federation/v1/versionif you delegate to port 443, orhttps://example.com:8448/_matrix/federation/v1/versionif you serve federation on 8448. The spec says it requires no authentication and is not rate-limited, and it returns{"server":{"name":"Synapse","version":"..."}}. On Pro, assert contains"name":"Synapse"(Conduit and Dendrite put their own names here). This is the check that fails when a firewall rule for 8448 disappears, when the proxy forwards the client paths but not the federation ones, or when a worker that handles federation dies. - The signing key. Optionally monitor
https://matrix.example.com/_matrix/key/v2/server, which is what other servers fetch to verify your signatures; a 200 with yourserver_namein the body proves the key is being served. Keep the key itself backed up (step 6); rotating it is painful.
If you also monitor the 8448 port with a TCP monitor (Pro), you can tell a closed port from a broken proxy: TCP green and HTTP red means the proxy or Synapse, TCP red means the firewall or the host. TCP port monitoring explains the type.
Step 4: a check that reaches the database (Pro)
Everything so far is served from memory. The simplest unauthenticated request that returns a row from PostgreSQL is a profile lookup: GET /_matrix/client/v3/profile/@monitor:example.com returns the account's displayname and avatar_url. Create a dedicated account, give it a displayname you will recognise (say Monitoring Canary), and add a monitor on that URL with the assertion contains "displayname":"Monitoring Canary". When PostgreSQL is down, the connection pool is exhausted, or the disk is full, this monitor fails while /health is still green, and the gap between the two is your diagnosis.
If you have set limit_profile_requests_to_users_who_share_rooms, anonymous profile lookups are refused and this check needs authentication. Use the heartbeat approach from step 5 with a script that logs in as the canary and reads its own profile instead.
Step 5: the federation tester, as an hourly heartbeat (Pro)
The Matrix federation tester at federationtester.matrix.org does what a remote homeserver does: resolves your server name through well-known and SRV, connects, checks the TLS certificate, fetches and verifies your signing keys, and reports. Its JSON API returns a top-level FederationOK boolean plus WellKnownResult, DNSResult, ConnectionReports, ConnectionErrors, and Version. It is the single most complete federation check available, and you should not hit a volunteer-run service every minute from an uptime monitor. Run it hourly from cron and report with a heartbeat:
# hourly: pass only if the federation tester says FederationOK is true
0 * * * * curl -fsS --max-time 60 "https://federationtester.matrix.org/api/report?server_name=example.com" \
| grep -Eq '"FederationOK":\s*true' \
&& curl -fsS --max-time 10 --retry 3 https://cronalert.com/api/heartbeat/YOUR-TOKEN Create a heartbeat monitor with an expected interval of 1 hour and a grace period of 30 minutes, and paste its ping URL into the last line. If the tester is unreachable or reports a failure, the ping is skipped and the heartbeat alerts; the tester's full report, opened in a browser, tells you which step failed. Heartbeat grace periods explains the sizing, and cron job heartbeats covers the pattern generally.
Step 6: backups (Pro)
Synapse's backup documentation is specific. The database dump is pg_dump -Fc --exclude-table-data e2e_one_time_keys_json synapse, there is no need to stop Synapse or Postgres while it runs, and you should never restore a dump over an existing database. Alongside it, keep homeserver.yaml and its auxiliary files, the signing key (losing it means every other server must learn a new one via old_signing_keys), and from the media store the local_content and local_thumbnails directories; remote_content, url_cache, and their thumbnails can be fetched again. Make the job report:
# /etc/cron.daily/synapse-backup
set -euo pipefail
DEST=/srv/backups/synapse/$(date +%F)
mkdir -p "$DEST"
sudo -u postgres pg_dump -Fc --exclude-table-data e2e_one_time_keys_json synapse > "$DEST/synapse.dump"
cp /etc/matrix-synapse/homeserver.yaml /etc/matrix-synapse/*.signing.key "$DEST"/
rsync -a /var/lib/matrix-synapse/media_store/local_content /var/lib/matrix-synapse/media_store/local_thumbnails "$DEST"/
# ship it off the host: rclone copy "$DEST" remote:synapse/$(date +%F) ...
curl -fsS --max-time 10 --retry 3 https://cronalert.com/api/heartbeat/YOUR-TOKEN Heartbeat expected interval 1 day, grace a few hours. set -e stops the script before the curl if any step fails, so a missed ping means a failed backup. Monitoring database backups adds a restore test; given the "never restore over an existing database" rule, the restore test for Synapse belongs on a scratch database.
Element Web and the certificate
If you host Element Web at https://element.example.com, it is a static site: a 200 on / is enough, and static site monitoring covers the rest. Its /config.json is where default_server_config points clients at your homeserver, so a Pro keyword monitor on that file for your base_url catches a deploy that ships the wrong config. Keep Element on its own hostname rather than the homeserver's, as Synapse's documentation recommends, for the usual cross-site reasons.
Certificates matter twice for Matrix. Clients refuse a bad certificate like any HTTPS app, and remote servers refuse to federate with a homeserver whose certificate is invalid for the name they resolved to, so an expired certificate on the federation host is a total federation outage with no error visible to your users. SSL monitoring is part of every HTTP monitor on every plan; the monitors from steps 1 to 3 already watch matrix.example.com and example.com. If federation lives on 8448 with a separate certificate, the 8448 monitor watches that one. Monitoring Let's Encrypt renewals covers the renewal job itself.
What not to monitor
- Media downloads. Since the authenticated-media change in 2024, media endpoints under
/_matrix/client/v1/media/need an access token, and the legacy unauthenticated/_matrix/media/paths are being frozen or disabled by default. An unauthenticated media check will break on an upgrade through no fault of your server. If you must, put a canary download in the step 5 heartbeat script with a token. - The admin API.
/_synapse/admin/v1/server_versionis handy for a human but the admin paths should be restricted at the proxy, and a monitor holding an admin token is a liability. Monitor that the path is blocked instead: an HTTP monitor on/_synapse/admin/v1/server_versionwith the expected status set to your proxy's refusal, usually 403, stays green while the restriction holds. The custom-headers guide explains inverse monitors. - Sync.
/_matrix/client/v3/syncneeds a token and is long-polling by design; an uptime monitor against it measures your timeout, not your server. - Public room directory.
/_matrix/client/v3/publicRoomsis unauthenticated if you allow it, but it is a heavier query than a profile lookup and disabled on most private servers. The profile check is the better database probe. - TURN/coturn. Voice and video depend on it, but it speaks UDP and TURN, not HTTP. A TCP monitor on the TURN TCP port proves the process is listening; actual relay health needs a heartbeat from a test client inside your network.
Reading an alert
| Alert | Likely cause | First thing to check |
|---|---|---|
/health timeout or connection error | Host, proxy, or Synapse process down | systemctl status matrix-synapse; proxy logs |
/health 502/503 | Proxy up, Synapse starting or crashed (often after an upgrade with a long migration) | Synapse log; disk space |
/health green, profile check red | PostgreSQL down, pool exhausted, disk full | pg_isready; Synapse log for could not connect |
/health green, /_matrix/client/versions red | Proxy route for /_matrix missing or pointed at a dead worker | Proxy config; which upstream handles /_matrix/client |
| Federation version red, everything else green | 8448 firewall, federation proxy block, or federation worker down | ss -ltnp | grep 8448; curl -v the URL from outside |
| Well-known keyword missing | Website deploy dropped the file, or content-type changed | curl -i https://example.com/.well-known/matrix/server |
| Federation-tester heartbeat overdue | DNS, certificate, signing key, or the tester itself unreachable | Open the tester report in a browser |
| Backup heartbeat overdue | Cron not running, pg_dump permission error, destination full | Run the script by hand |
Frequently asked questions
Does /health exist on Conduit, conduwuit, or Dendrite?
Not as a Synapse-compatible guarantee. Use /_matrix/client/versions and /_matrix/federation/v1/version as the primary checks on those servers; both are specification endpoints that every implementation serves without authentication.
Will checking every minute bother the server?
No. /health is deliberately excluded from the request log, the versions and federation endpoints return small constants, and a profile lookup is a single indexed query. The federation tester is the exception, which is why it runs hourly from cron rather than from a monitor.
My homeserver is on a Tailscale network or a bare IP. Can I still monitor it?
A federating homeserver needs a public hostname by definition, so the HTTP monitors apply. For a private, non-federating server, use the heartbeat pattern: a cron job inside the network runs curl --fail against /health and /_matrix/client/versions, then pings a heartbeat URL. Monitoring services on a VPN covers it; bare IPs cannot be HTTP monitor targets at all.
Should I put the homeserver on a status page?
Yes, especially for a community server. Every plan includes a public status page; show the /health and federation monitors, and users on other servers can check whether you are the problem before messaging you somewhere else.
Which parts need a paid plan?
The /health, versions, and federation version monitors, the certificate monitoring, and the inverse admin-path monitor are free, status-only. The keyword assertions on the well-known files and the profile check, the TCP monitor on 8448, and the two heartbeats need Pro at $5/month.
Get started
Three free monitors (/health, /_matrix/client/versions, /_matrix/federation/v1/version) catch the outages your users and the rest of the network are hiding from you. Create a free CronAlert account and add them first; the well-known assertions, the profile check, and the backup heartbeat come next. Related reading: monitoring a Mastodon instance, monitoring Docker self-hosted services, monitoring behind a reverse proxy, DNS monitoring, and SSL certificate monitoring.