/ guides / api-health-check-monitoring
API health check monitoring: design a /health endpoint worth watching
How to design an API health check endpoint worth monitoring, from shallow vs deep checks to status codes, then watch it with status and keyword checks.
Updated
API health check monitoring means requesting a dedicated health endpoint on a schedule from outside your network and alerting when it stops answering correctly. Fettle is uptime monitoring for developers, and its HTTP monitor does that check: it sends the request you configure, checks the status code against the codes you expect, and can require a keyword in the response body. The check is only as good as the endpoint behind it, so this guide starts with the endpoint.
What should a health check endpoint actually check?
A health endpoint worth monitoring answers one question: can this API serve a real request right now? That means it runs through the same process, framework and critical dependencies a real request uses, such as the primary database, and leaves out anything the API can work without. It returns fast, a clear status code, and a tiny body.
There are two kinds of health check, and they serve different callers:
- Shallow (liveness). The process is up and its HTTP stack answers. No dependencies touched. This is what an orchestrator wants before restarting a container: Kubernetes' liveness probes restart the container when they fail, so a database outage must not fail them, or every replica restarts at once.
- Deep (readiness). The process can do its job: it reaches the database, the cache it cannot live without, the queue it writes to. A failing readiness probe takes the instance out of rotation without restarting it.
An external uptime monitor wants the deep kind. A shallow 200 OK keeps answering while the
connection pool is exhausted and every real request returns 500. Many teams expose both:
/livez for the orchestrator and /healthz (or /health) for monitoring.
What a deep check should and should not include:
- Include the dependencies without which every request fails: run one trivial query against
the primary database (
SELECT 1), one round trip to a cache the request path needs. - Exclude what degrades a feature but not the API: an email provider, an analytics sink, a third-party API behind a single endpoint. If those fail the health check, you get paged for someone else's outage.
- Bound every dependency call with its own short timeout, well under the monitor's timeout, so a hanging database turns into a fast 503 instead of a timed-out check with no detail.
Which status codes should a health endpoint return?
Return 200 when the API is healthy and 503 Service Unavailable when a critical dependency is down. MDN describes 503 as the server not being ready to handle the request, which is exactly the claim a failed health check is making. The status code is the signal; the body is detail.
Some habits that keep the signal clean:
- Do not return 200 with
"status": "error"in the body. Most monitors, load balancers and humans read the status code first. If you want a body-level signal too, see the keyword section below. - Do not redirect. A health URL that 301s to a login page, or to a trailing-slash variant, makes the check depend on the redirect chain. Serve the endpoint at the exact URL you monitor.
- Keep auth optional or simple. A health endpoint behind a full session flow cannot be checked. A static bearer token in a header is enough if you want it private.
How do I keep a health endpoint from leaking internals?
Return only what a monitor needs: a status word, and maybe a timestamp. No versions, hostnames,
internal IPs, stack traces, connection strings or raw exception messages. A public health
endpoint is readable by anyone who guesses the path, and /health is not a hard guess.
{"status":"ok"}
If you want per-dependency detail for your own debugging, put it behind the same token the monitor sends and keep the public answer to the status word. Error text from a database driver often includes the host and database name.
How cheap does a health check need to be?
Cheap enough that running it from several regions at your monitor's interval is noise in your metrics. Fettle intervals go down to 30 seconds, and the plan floors are every 3 minutes on Free, every minute on Plus and Pro and every 30 seconds on Max for up to 50 monitors (every minute for the rest). Each region sends its own request, so a monitor that runs every 30 seconds from a pair of regions sends four requests a minute.
A SELECT 1 and a cache ping cost almost nothing at that rate. What does cost something:
health checks that count table rows, scan a queue, warm caches or call external APIs. If
a dependency check is expensive, cache its result in the process for a few seconds and serve the
cached verdict.
Rate limiters and bot rules are the other cost. Unless you set your own, Fettle's probes send a
User-Agent that starts with Fettle-Monitor/1.0, which gives a WAF rule something stable to let
through.
How do I monitor a health endpoint with Fettle?
Create an HTTP monitor that points at the health URL, expects the status codes your endpoint
returns when healthy, and optionally requires a keyword in the body. One POST /v1/monitors
request with a write-scoped API key does it, on every plan including Free.
The uptime monitoring API page lists what else the API covers.
curl -s -X POST https://api.fettle.sh/v1/monitors \
-H "Authorization: Bearer fettle_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "API health",
"type": "HTTP",
"target": "https://api.example.com/healthz",
"interval_seconds": 60,
"timeout_seconds": 10,
"regions": ["us-east", "eu-central"],
"confirmation_threshold": 2,
"config": {
"method": "GET",
"expected_status_codes": [200],
"keyword": "\"status\":\"ok\"",
"request_headers": { "Authorization": "Bearer health-probe-token" },
"follow_redirects": false,
"verify_ssl": true
}
}'
name, type, target, interval_seconds, timeout_seconds and regions are required; the
rest have defaults. Field by field:
expected_status_codesis a list of HTTP codes from 100 to 599,[200]by default. Any other code fails the check, and the incident names the cause: a 4xx, a 5xx or an unexpected status.keywordmust appear in the response body for the check to pass. More on it below. AHEADrequest has no body, so a keyword is refused withHEAD.timeout_secondsis 1 to 120. A request that runs past it fails as a timeout. Set it above your endpoint's slowest healthy answer and below the interval.follow_redirectsis on by default and follows up to 10 hops; a longer chain fails as too many redirects. With it off, the redirect response itself is what gets checked, so a 301 fails unless 301 is inexpected_status_codes.verify_sslis on by default: an expired certificate, an unknown issuer or a hostname mismatch fails the check. Turn it off only for a target you know uses a private certificate, and watch expiry with an SSL monitor instead.regionsandconfirmation_thresholddecide when a failure becomes an alert. With two or more regions, the monitor goes DOWN only when every region with a fresh result fails for that many rounds in a row. The multi-region monitoring guide explains the rule and its trade-off.
Region count and interval floor depend on the plan: this example needs a plan with a one-minute floor that allows more than one region. The pricing page has both for each plan, and the free multi-region check runs one request against your URL from every region before you commit to a monitor.
How does the keyword match work?
The keyword is a case-sensitive substring searched in the first 1 MiB of the response body. No JSON parsing, no regular expressions: the check passes if those exact bytes appear anywhere in the body, and fails as "keyword missing" if they do not. That makes it a way to fail a check that returns 200 with the wrong content.
The pitfall is whitespace. "status":"ok" and "status": "ok" are different bytes. JavaScript's
JSON.stringify
writes no spaces by default; Python's json.dumps
writes a space after every colon by default. Copy the keyword from a real response
(curl -s https://api.example.com/healthz), not from your mental picture of it. Inside the JSON
config, escape the inner quotes, as in the example above.
The keyword also lets the body carry a second signal. An endpoint that returns 200 with
{"status":"degraded"} when an optional dependency is down passes a status-only check but fails a
keyword check for "status":"ok". Decide which behavior you want before you pick the keyword.
Can I monitor an endpoint that needs POST or authentication?
Yes. Set method to POST, PUT, PATCH or DELETE and put the payload in request_body, up
to 8,192 bytes. Add up to 20 request_headers for auth tokens or API keys. Header values are
masked when you read the monitor back, so a token stored in a monitor is not shown again in the
dashboard or the API.
{
"method": "POST",
"expected_status_codes": [200, 201],
"request_headers": { "X-Api-Key": "health-probe-key" },
"request_body": "{\"probe\":true}",
"keyword": "\"ok\":true"
}
When the body is valid JSON and you set no Content-Type, the probe sends
application/json. A monitor read back shows each header name with a placeholder value; send the
placeholder back unchanged on an update to keep the stored secret, or send a new value to replace
it. Use a token scoped to the health endpoint only: it is a credential that lives in a monitoring
tool.
What about background workers that have no endpoint?
A health endpoint says nothing about the queue consumer, the scheduler or the nightly job beside the API. For those, Fettle has heartbeats: the worker calls a ping URL each time it finishes a unit of work, and Fettle opens an incident when the ping stops arriving on time.
A job can also call the URL's /fail path to report a failure at once. The
heartbeats docs cover the setup, and the
cron job monitoring guide walks through one end to end.
A health check plus a heartbeat per background process covers both directions: Fettle reaching your API, and your workers reaching Fettle. The wider picture is in the uptime monitoring guide, the rest of the series is on the guides hub, and the first API call is in the quickstart.
Questions
- Should a health check endpoint test the database?
- For external monitoring, yes, with one cheap query and a short time limit. A check that never touches the database stays green while every real request fails. Keep slow or optional dependencies out of the verdict.
- What status code should an unhealthy API return?
- Return 200 when healthy and 503 Service Unavailable when a dependency the API cannot work without is down. A monitor that expects 200 then fails on the 503 without needing to parse the body.
- Is Fettle's keyword match case-sensitive?
- Yes. The keyword is a plain, case-sensitive substring searched in the first 1 MiB of the response body. Whitespace counts too, so match the exact bytes your API sends, not a pretty-printed version.
Start monitoring in a minute
The free plan checks up to 20 monitors as often as every 3 minutes from 1 region, and emails you when one breaks. See pricing for the paid intervals, or follow the quickstart to do it from the API.