---
title: "Health check endpoint best practices | Logdash"
description: "What a health endpoint should check, what it must never check, why it returns 503 and not 500, and how to point a monitor at it in three steps."
url: https://logdash.io/health-check
---

# 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

```bash
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** Add a monitor in Logdash and give it the address of the endpoint you just wrote. Five HTTP monitors 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.

## All health check guides

[Health check endpoint in Next.js](https://logdash.io/health-check/nextjs): Create app/api/health/route.ts with a GET handler that runs one cheap query against your database, returns 200 when it succeeds and 503 when it throws, and add export const dynamic = "force-dynamic" so the response is never served from a cache.

[Express health check endpoint](https://logdash.io/health-check/express): Add a route that answers 200 when the process is up and a second one that runs select 1 against your database and answers 503 when the query fails, then point a monitor at the second one.

[SvelteKit health check endpoint](https://logdash.io/health-check/sveltekit): Create src/routes/health/+server.ts, export a GET handler that runs one query and returns json({ ok: true }) or a 503 when the query throws, and set export const prerender = false so the route is built as a real endpoint.

[Nuxt health check endpoint](https://logdash.io/health-check/nuxt): Add server/api/health.get.ts with a defineEventHandler that runs one query, returns a small object on success, and calls setResponseStatus(event, 503) before returning when the query throws.

[NestJS health check endpoint with Terminus](https://logdash.io/health-check/nestjs): Install @nestjs/terminus, inject HealthCheckService and TypeOrmHealthIndicator into a /health controller, and the endpoint returns 200 while every indicator is up and 503 the moment one goes down.

[FastAPI health check endpoint](https://logdash.io/health-check/fastapi): Add an async /health route that runs SELECT 1 through the session with a short timeout and returns a JSONResponse with status\_code=503 when the query fails, so a live Uvicorn process with a dead database reads as down.

[Django health check endpoint](https://logdash.io/health-check/django): Write a view that runs SELECT 1 on the default connection and returns JsonResponse with status=503 when it raises, wire it into urls.py, or install django-health-check if you want the batteries-included version.

[Flask health check endpoint](https://logdash.io/health-check/flask): Register a blueprint with a /health route that runs db.session.execute(text("SELECT 1")) and returns a 503 when it raises, so the check fails whenever the database the app depends on is gone.

[Rails health check endpoint and what /up actually checks](https://logdash.io/health-check/rails): Rails 8 already serves a health check at /up, but it only returns 200 when the app boots without raising, so if you want the database checked you write your own controller and return 503 when the query fails.

[Laravel health check endpoint and the built-in /up route](https://logdash.io/health-check/laravel): Laravel 11 and 12 register a health route at /up out of the box, but it only proves the framework booted, so add a route that runs one database query and returns 503 when it fails.

[Elixir Phoenix health check endpoint with Ecto and the cluster](https://logdash.io/health-check/phoenix): Phoenix ships no health route, so you write a controller action that runs one Ecto query, returns 200 with the number of connected nodes, and answers 503 when the query raises.

[Spring Boot health check endpoint with Actuator](https://logdash.io/health-check/spring-boot): Add spring-boot-starter-actuator and Spring Boot serves /actuator/health for you, returning 200 while every contributor is UP and 503 the moment one of them goes DOWN.

[.NET health check endpoint in ASP.NET Core](https://logdash.io/health-check/dotnet): ASP.NET Core has health checks built in: AddHealthChecks() registers them, MapHealthChecks("/health") serves them, and an unhealthy result returns 503 with no extra configuration.

[Go health check endpoint with net/http](https://logdash.io/health-check/go): Write it yourself: an http.HandlerFunc that runs db.PingContext under a one-second context.WithTimeout and writes http.StatusServiceUnavailable when the ping fails.
