Health check endpoint best practices
A health check endpoint should test the dependencies a real request needs, usually one cheap database query, and answer in under a second with 200 when the service can serve traffic and 503 when it cannot.
Most health endpoints return a hardcoded 200 from inside the process. That proves one thing: the process is running and the HTTP server accepted a connection. It says nothing about the database being reachable, the connection pool having a free slot, or the migration from the last deploy having finished. So the monitor stays green while every real request returns 500, and you hear about the outage from a customer.
One rule fixes this and the rest of the page is detail: a health check should fail for the same reasons a real request fails. Not more, not fewer. An endpoint that cannot go down is not a health check, it is a decoration.
What to check
- The database, with one cheap query. A select 1 is enough. It proves the pool handed out a connection and the server answered. It must never scan a table.
- The cache or queue the request path cannot work without. Usually Redis. A ping, not a read.
- Anything local the process would fail without: a writable upload directory, a secret loaded at boot.
- That is the list. Check what a request needs, the cheapest way that would still catch a real failure, and stop.
What not to check
- Third-party APIs you do not control. Stripe having a bad afternoon is not your service being down. A check that fails on it pages you at 4am for someone else's outage while your app serves traffic fine.
- Every downstream microservice. Chained health checks turn one slow neighbour into a fleet-wide red board, then into a restart storm worse than the outage you started with.
- Anything billed per call. A monitor is thousands of requests a day, forever. A health check should not arrive with an invoice.
- Anything slow. Every dependency you add is another second of latency and another way to trip your monitor's timeout.
- The deep version. If you want a full dependency audit, put it behind a separate path and run it on demand, not on every check.
Status codes
Return 200 when the service can serve traffic and 503 when it cannot. That is the whole contract. Not 500, which reads as the check crashing rather than the service being unavailable. And not a 200 carrying an unhealthy field in the body, because most uptime monitors and every load balancer read the status line and nothing else, so a 200 with a sad body is an outage nobody hears about. Logdash flips a monitor to down on any status outside 200-399 and sends the alert on the transition, so the status line is heard and the body is not.
Liveness and readiness are different questions
Liveness asks whether the orchestrator should kill this process. Readiness asks whether traffic should be routed to it. Different questions, and not the same endpoint. A pod whose database is unreachable is not broken - restarting it brings nothing back - so it fails readiness and passes liveness. A pod that has deadlocked should fail liveness. Kubernetes wants both: one path proving the event loop still turns, another checking the dependencies. An external uptime monitor wants readiness, because it asks the same question your users ask.
The two responses
curl -sS -o /dev/null -w '%{http_code} in %{time_total}s\n' \
https://api.example.com/health
# healthy: the database answered, keep sending traffic
# 200 in 0.084s
# unhealthy: the database did not, take me out of rotation
# 503 in 0.096s Practical rules
- Keep it under a second. The Logdash pinger gives up after 10 seconds and records the check as down, so an endpoint that waits on a slow dependency invents its own outage. One cheap query, a short timeout, no retries.
- Keep the body boring. No version numbers, no environment names, no hostnames, no connection strings, no stack traces. A public endpoint that lists your stack is free reconnaissance. One ok field is a complete answer.
- Do not authenticate it if an external monitor has to reach it. An auth flow is one more thing that breaks and wakes you for nothing. Put it on a boring path instead.
- Send Cache-Control: no-store. A CDN, a reverse proxy or a framework's static optimiser will happily cache one 200 and keep serving it for an hour after the service died.
- Pick one path and never move it. The path /health is the safe guess. Write it in the README, because the person wiring up the monitor at 2am is not always you.
- Exclude it from request logs and rate limits, or the check will drown the log volume you need during an incident.
Point a monitor at it
- 1 Add the URL Create a project in Logdash and give it the address of the endpoint you just wrote. One HTTP monitor per project, five projects on the free plan. Every check records the status code and response time, so the latency chart fills itself in.
- 2 Pick the interval Every 5 minutes on the free plan, every minute on Builder, every 15 seconds on Pro. Choose the gap you can live with. At 15 seconds that is 5,760 checks a day, which is the real reason to keep the query cheap.
- 3 Break it on purpose Stop the database and leave the process running. The endpoint should start returning 503, the monitor should flip to down on the next check, and a Telegram alert should land naming the endpoint and the status code. If nothing arrives, you found the bug today instead of during the outage.
What path should a health check endpoint use?
The path /health is the convention and the one anyone will guess first. Kubernetes setups tend to use /healthz for liveness and /readyz for readiness. Any of them works as long as it is stable, reachable without auth, and written down somewhere. The path matters far less than never moving it.
What status code should a health check return?
200 when the service can serve traffic, 503 when it cannot, and nothing else. 500 reads as the check itself crashing, and a 200 with an unhealthy body is read as healthy by every load balancer and most monitors, including Logdash, which flips to down on any status outside 200-399.
Should a health check hit the database?
Yes, with one cheap query such as select 1. A service that cannot reach its database cannot serve requests, so a check that skips it stays green through the exact outage you most want to hear about. Do not query a real table: you are testing the connection, not the data.
Should a health check endpoint be authenticated?
Not if an external uptime monitor has to reach it. Authentication is one more moving part that can fail and wake you up for nothing. Keep versions, hostnames and config out of the body and the endpoint is dull enough to leave open. If policy demands it, use a static header token the monitor can send.
How often should a health check run?
As often as the delay you can tolerate. A 5-minute interval means up to 5 minutes of downtime before anyone knows; one minute suits most side projects; 15 seconds is for anything taking payments. Logdash checks every 5 minutes on the free plan, every minute on Builder and every 15 seconds on Pro.