Search is the feature users notice first and report last. When the search server is down, the box returns an error and people assume they typed something wrong. When it is up but has stopped indexing, results are simply a day old and nobody files a ticket about it. Both Meilisearch and Typesense are easy to self-host and reliable in normal operation, which is exactly why their failures go unnoticed: the process rarely crashes, so the failure is usually a full disk, an exhausted memory budget, a stuck indexing task, or a key rotation that broke the application's credentials while the health endpoint stayed green.

This guide sets up external monitoring for either engine, from the health endpoint to a search canary that proves real results are coming back. Status checks and custom headers are on every CronAlert plan; keyword assertions and heartbeats are Pro ($5/month) and are marked. The examples use search.example.com as the public hostname of a Meilisearch on port 7700 or a Typesense on port 8108 behind a reverse proxy.

What each engine exposes

MeilisearchTypesense
HealthGET /health, no key, {"status":"available"}; 500 mustRestart if a restart is neededGET /health, no key, {"ok":true}; adds resource_error when out of disk or memory
Auth headerAuthorization: Bearer <key>X-TYPESENSE-API-KEY: <key>
GET searchGET /indexes/<index>/search?q=… (documented as discouraged for apps, fine for a monitor)GET /collections/<coll>/documents/search?q=…&query_by=…
Search-only keyDefault Search API Key, or a custom key with the search action on named indexesKey with actions: ["documents:search"] on named collections
VersionGET /version (key required)GET /debug → {"state":1,"version":"…"} (key required)
Backlog / statsGET /tasks?statuses=enqueued, GET /stats (admin-scope key)GET /stats.json, GET /metrics.json (key required)
BackupPOST /dumps (async task); snapshots via --schedule-snapshotPOST /operations/snapshot?snapshot_path=…

Two things stand out. First, /health is the only route on either engine that answers without a key, so it is the only status-only check you can run without storing a credential. Second, both engines have a GET search route and both accept the key in a header, which means an ordinary HTTP monitor with custom headers can run a real query and assert on the result.

Step 1: the /health monitor

Create an HTTP monitor on https://search.example.com/health, method GET, expected status 200, timeout 10 seconds. Meilisearch's documentation says the engine returns available with a 200 when healthy and mustRestart with a 500 when it needs a restart, so the status alone catches the restart case. Typesense returns {"ok":true}; when a node is running out of memory or disk the body gains a resource_error field set to OUT_OF_DISK or OUT_OF_MEMORY, and Typesense starts rejecting writes. On Pro, make the Typesense monitor a not_contains assertion for resource_error; that is the single most valuable line in this guide for a Typesense operator, because a full disk is the normal way a healthy-looking node stops accepting documents.

Because the check goes through the public hostname, a green result also covers DNS, TLS, and the reverse proxy. The interval is set by your plan, every 3 minutes on Free or every minute on Pro, and alerts go to your team's channels. If the engine is reachable only from inside your network, skip to the heartbeat section.

Step 2: a search canary (Pro)

A healthy process can serve an empty or stale index. The check that matters to users is a query that returns the right document. Create a key that can only search:

  • Meilisearch: use the Default Search API Key (listed by GET /keys with the master key), or create a key whose actions is ["search"] and whose indexes names the one index. The documentation describes the Default Search API Key as the one to use from the frontend, which is the right trust level for a monitor.
  • Typesense: create a key with "actions": ["documents:search"] and "collections": ["products"]. Only the create response shows the full key, so record it then.

Then create a monitor on the GET search route with the key in a custom header and a keyword assertion for a document that must match:

MeilisearchTypesense
URLhttps://search.example.com/indexes/products/search?q=anchor%20widget&limit=1https://search.example.com/collections/products/documents/search?q=anchor%20widget&query_by=name&per_page=1
Header{"Authorization":"Bearer <search key>"}{"X-TYPESENSE-API-KEY":"<search key>"}
Assert contains"sku":"ANCHOR-001""sku":"ANCHOR-001"

Pick an anchor document that never changes, or better, create one specifically for monitoring with an unusual name so the query is cheap and the match unambiguous. A Meilisearch response carries hits, query, processingTimeMs, and estimatedTotalHits; a Typesense response carries found, hits, and search_time_ms. Assert on the document field rather than on found or the hit count, since those are present even when the hit is wrong. The keyword monitoring guide covers match types; each monitor carries one assertion.

This monitor fails when the index was dropped and recreated empty, when a reindex job wiped documents and died halfway, when the key was rotated and your monitor's copy is now invalid (a 403, which is a useful alarm in itself because the application's copy is probably invalid too), or when the engine answers /health but has stopped serving queries. A 404 index_not_found from Meilisearch is the "someone deleted the index" alert.

Step 3: catch a stuck indexer (Pro)

The quiet failure is an indexer that stopped. Documents keep being written to your database, the sync job that pushes them to the search engine is dead, and every search returns yesterday. Two approaches, in order of preference:

  1. A freshness canary. Have the sync job also upsert a marker document, {"id":"monitor-marker","day":"2026-11-10"}, with today's date in a filterable or searchable field. Then a second search monitor asserts on the current day. That requires updating the monitor daily, which is clumsy, so the practical version is: make the sync job itself ping a heartbeat only after it has verified its last write is searchable. A dozen lines of script: write the marker, search for it, ping if found.
  2. Meilisearch's task queue. Every write to Meilisearch is an asynchronous task. GET /tasks?statuses=enqueued,processing returns a total that should hover near zero on a healthy instance, and GET /tasks?statuses=failed should not grow. Both need a key with task permissions, so run them from a cron script on your own host and gate a heartbeat on total being below a threshold and no failed task in the last hour. Typesense writes are synchronous, so there is no equivalent queue; a write that fails returns its error to your application, which is where that alert belongs.
# every 10 minutes: Meilisearch backlog + failed-task check → heartbeat
*/10 * * * * \
  BACKLOG=$(curl -fsS --max-time 10 -H "Authorization: Bearer $MEILI_ADMIN_KEY" \
    "https://search.example.com/tasks?statuses=enqueued,processing&limit=1" | jq .total) \
  && FAILED=$(curl -fsS --max-time 10 -H "Authorization: Bearer $MEILI_ADMIN_KEY" \
    "https://search.example.com/tasks?statuses=failed&afterFinishedAt=$(date -u -d '-1 hour' +%FT%TZ)&limit=1" | jq .total) \
  && [ "$BACKLOG" -lt 100 ] && [ "$FAILED" -eq 0 ] \
  && curl -fsS --max-time 10 --retry 3 https://cronalert.com/api/heartbeat/YOUR-TOKEN

Heartbeat expected interval 10 minutes, grace 10 to 15 minutes. Grace periods explains the sizing, and diagnosing heartbeat alerts shows how to read a missed ping.

Step 4: Typesense clusters and resource limits

A production Typesense is usually a 3-node cluster, which the documentation says is the minimum to tolerate one node failing. Each node answers /health for itself, reads are served by whichever node receives them, and writes are forwarded to the leader. Monitor each node's /health individually, through its own hostname, as well as the load-balanced hostname your application uses: a cluster that has lost one node is still green on the balanced hostname and is one more failure from an outage. GET /debug (key required) reports state 1 for the leader and 4 for a follower; a cron heartbeat that asserts exactly one node reports state 1 catches the split-brain and no-leader cases, and a node that is still catching up after a restart answers 503, so a temporary red on one node after a deploy is expected.

GET /metrics.json gives system_disk_used_bytes and typesense_memory_allocated_bytes; a heartbeat that stops pinging above 85% disk gives you the warning before resource_error appears. These endpoints need a key, which is why they live in a script rather than a monitor.

Step 5: dumps and snapshots (Pro)

Meilisearch has two backup forms. Snapshots are an exact copy of the data directory for the same version and can be scheduled at launch with --schedule-snapshot (the documentation suggests a daily snapshot as a starting point); dumps are portable across versions and are created with POST /dumps, which enqueues a task and writes a file to the dumps directory when the task succeeds. Typesense snapshots are POST /operations/snapshot?snapshot_path=/path, which returns {"success":true}. Either way, the backup is a job, and the job should prove it ran:

# /etc/cron.daily/meilisearch-dump
set -euo pipefail
H="Authorization: Bearer $MEILI_ADMIN_KEY"
UID_=$(curl -fsS --max-time 10 -X POST -H "$H" https://search.example.com/dumps | jq .taskUid)
for i in $(seq 1 60); do
  S=$(curl -fsS --max-time 10 -H "$H" "https://search.example.com/tasks/$UID_" | jq -r .status)
  [ "$S" = succeeded ] && break
  [ "$S" = failed ] && exit 1
  sleep 10
done
[ "$S" = succeeded ]
# ship the newest file in the dumps directory off the host, e.g. rclone copy /var/lib/meilisearch/dumps remote:meili/
curl -fsS --max-time 10 --retry 3 https://cronalert.com/api/heartbeat/YOUR-TOKEN

Create a heartbeat with an expected interval of 1 day and a few hours of grace. The Typesense version replaces the dump call with the snapshot request and an rsync of the snapshot path. Meilisearch's documentation notes it never writes a partial dump file, so a file that exists is a complete one; the heartbeat covers the case where no file was written at all. Monitoring database backups covers restore testing.

Certificates, proxies, and private instances

SSL monitoring is part of every HTTP monitor on every plan, so the /health monitor already watches the certificate on search.example.com; if the browser talks to the search engine directly (common with Meilisearch's frontend-key model), an expired certificate is a broken search box on every page, not just a failed backend call. Monitoring behind a reverse proxy covers the proxy failures that look like engine failures. For an engine that is only reachable inside your network, run the same curl checks from a cron job on a host that can reach it and report with a heartbeat, as in monitoring Docker self-hosted services; a TCP monitor on 7700 or 8108 tells you the port is open if the host has a public address.

What not to monitor

  • /version or /debug from a monitor. They need a key, and the information rarely changes. If you want to know about upgrades, a content changed monitor (Pro) on /version with the key in a header alerts exactly once per upgrade, which some teams like; most do not need it.
  • A search with no assertion. A 200 from the search route with zero hits proves nothing. Always assert on a document field.
  • Indexing latency from outside. HTTP monitors have no percentile or latency alerting; the timeout is the only slowness alarm. If query latency matters, processingTimeMs and search_time_ms belong in your own metrics, not in an uptime check.
  • Admin routes with the master key. Never put the master key or an admin key in a monitor's headers. Everything that needs one runs from a script and reports via heartbeat.

Reading an alert

AlertLikely causeFirst thing to check
/health timeout or connection errorHost, container, or proxy downContainer status; proxy logs
Meilisearch /health 500mustRestart: the engine wants a restartRestart it; check logs for the cause
Typesense resource_error presentDisk or memory exhausted; writes rejectedDisk usage; memory limit on the container
Search canary 403 or 401Key rotated or revokedWhether the application's key still works
Search canary 404Index or collection deleted or renamedReindex job logs
Search canary 200, keyword missingIndex emptied or anchor document goneDocument count via /stats or the collection endpoint
Backlog heartbeat overdueMeilisearch task queue growing or tasks failingGET /tasks?statuses=failed for the error
One Typesense node red, balanced hostname greenNode down or catching up after restart/debug on the remaining nodes for leader state
Dump or snapshot heartbeat overdueCron not running, dump task failed, disk fullRun the script by hand

Frequently asked questions

Will a search every minute affect the engine?

A single-term query with limit=1 is among the cheapest requests either engine serves, far below one real user. The admin checks (tasks, stats, metrics) run every 10 minutes from cron. Nothing here will show up in your latency graphs.

Does this apply to Meilisearch Cloud or Typesense Cloud?

The health and search monitors do; both hosted services expose the same API. The backup heartbeats do not (Meilisearch Cloud does not allow manual dump export, and both services manage backups for you), and the cluster checks are the provider's job.

Can I monitor Elasticsearch or OpenSearch the same way?

Partly: GET /_cluster/health returns a status you can assert on with a keyword ("status":"green"), and a GET search with q= works with basic auth in a header. The details differ enough that they deserve their own post; the pattern of health plus search canary plus backup heartbeat is the same.

Which parts are free?

The /health monitors with custom headers and SSL monitoring are free. The search canary's keyword assertion, the resource_error not_contains check, and the heartbeats need Pro at $5/month.

Get started

One free monitor on /health catches the engine being down; one Pro search canary catches the far more common case of the engine being up with the wrong answers. Create a free CronAlert account, add the health monitor, then create a search-only key and add the canary. Related reading: monitoring Redis, PostgreSQL replication lag, API endpoint monitoring, third-party dependency monitoring, and why a monitor can say UP while the feature is broken.