Quick Answer
Every production app needs a /health endpoint that returns a 200 status code when the service is healthy. Without one, your load balancer, container orchestrator, and monitoring tools have no way to know if your app is running. AI code generators almost never create health endpoints. Add one that checks your critical dependencies (database, Redis, external APIs) and returns a structured response with status and latency for each.
Why Health Endpoints Matter
A health endpoint is the single most important operational feature in a production application. Every piece of infrastructure depends on it:
- Load balancers (AWS ALB, Nginx, Caddy) use health checks to route traffic only to healthy instances
- Container orchestrators use health checks to catch containers that are running but stuck: Kubernetes restarts a container that fails its liveness probe, and Docker marks it unhealthy
- Monitoring tools (Uptime Kuma, Datadog, Pingdom) use health checks to alert you when something is down
- Zero-downtime deploys wait for the new container to pass health checks before stopping the old one
Without a health endpoint, your infrastructure is flying blind. Without a health check, a failed container can go unnoticed until users report the problem, which drags out recovery. The Uptime Institute's Annual Outage Analysis 2024 found that 54% of survey respondents said their most recent significant, serious or severe outage cost more than $100,000, and 16% said it cost more than $1 million.
Health checks also decide what happens during a cloud outage. When a power failure took a Google Cloud zone in London offline on 20 October 2026, traffic had to move to the healthy zones. Read what the Google Cloud London outage shows about failover.
What AI Code Generators Miss
When you ask Bolt, Cursor, or Lovable to build a web app, you get routes for your features but no operational routes. AI tools build what you describe in your prompt. Nobody prompts "and add a health check endpoint" - so it does not exist.
// ❌ BAD - AI-generated Express app with no health endpoint
const app = express();
app.use(express.json());
app.get('/api/users', getUsers);
app.post('/api/users', createUser);
app.get('/api/posts', getPosts);
app.listen(3000);
// Load balancer has no way to check if this is alive
// ✅ GOOD - Health endpoint with dependency checks
const withTimeout = (promise, ms = 2000) =>
Promise.race([
promise,
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), ms)),
]);
app.get('/health', async (req, res) => {
const checks = {};
// Check database
try {
const dbStart = Date.now();
await withTimeout(db.query('SELECT 1'));
checks.database = { status: 'ok', latency: Date.now() - dbStart };
} catch (err) {
checks.database = { status: 'error', error: err.message };
}
// Check Redis
try {
const redisStart = Date.now();
await withTimeout(redis.ping());
checks.redis = { status: 'ok', latency: Date.now() - redisStart };
} catch (err) {
checks.redis = { status: 'error', error: err.message };
}
const allHealthy = Object.values(checks).every(c => c.status === 'ok');
res.status(allHealthy ? 200 : 503).json({
status: allHealthy ? 'healthy' : 'unhealthy',
uptime: process.uptime(),
timestamp: new Date().toISOString(),
checks
});
});
Liveness vs Readiness Checks
Docker and Docker Compose have a single healthcheck, which marks the container healthy or unhealthy. Kubernetes distinguishes between two types of health checks (plus a startup probe for slow-starting apps), and conflating them causes production incidents:
| Check Type | What It Answers | On Failure | Endpoint |
|---|---|---|---|
| Liveness | Is the process alive and responding? | Container is killed and restarted | /health or /healthz |
| Readiness | Is the app ready to serve traffic? | Traffic is stopped, but container stays alive | /ready |
Critical mistake: If your liveness check queries the database and the database is temporarily down, the orchestrator kills and restarts your app container repeatedly - making the outage worse. The liveness check should verify the process is alive (in-memory check). The readiness check should verify dependencies are available.
// Liveness - is the process alive? (lightweight, no external deps)
app.get('/healthz', (req, res) => {
res.status(200).json({ status: 'alive' });
});
// Readiness - can this instance serve traffic? (checks dependencies)
app.get('/ready', async (req, res) => {
try {
await db.query('SELECT 1');
await redis.ping();
res.status(200).json({ status: 'ready' });
} catch {
res.status(503).json({ status: 'not ready' });
}
});
Docker Compose Health Check Configuration
Docker Compose has built-in health check support that works with your endpoint. The check runs inside the container, so the command must exist in your image: the Alpine Node images ship wget but not curl, and the slim images ship neither.
# docker-compose.yml
services:
api:
build: .
ports:
- "3000:3000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:15
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
Common Mistakes
- Checking auth on the health endpoint. Health checks must be unauthenticated. Your load balancer does not have a JWT token. Keep
/healthpublic but do not expose sensitive internal details. - Slow health checks. If your health check takes 5 seconds because it runs a complex database query, your orchestrator will think the service is down. Use
SELECT 1orPING, not a full table scan. - No timeout on dependency checks. If your database is hanging (not refusing connections, just not responding), the health check hangs too. Always set a timeout on dependency checks (1-2 seconds max).
- Returning 200 when unhealthy. Some implementations return
{ status: 'error' }with a 200 status code. Load balancers check the HTTP status code, not the body. Return 503 when unhealthy.
VibeDoctor's Vibe Check looks through the code of server projects (Express, Fastify, Flask, Django, FastAPI, Rails, Spring Boot, Laravel, Go routers and others) for a health route such as /health, /healthz or /ping, and reports a low-severity configuration issue (CFG-008) when it finds none. Free to sign up.
FAQ
Should the health endpoint be authenticated?
No. Health checks are called by infrastructure (load balancers, orchestrators, monitoring) that does not have user credentials. The endpoint should be unauthenticated but should not expose sensitive information like database connection strings or internal IP addresses. Return status and latency, not configuration details.
What URL path should I use?
The most common conventions are /health, /healthz (Kubernetes convention), and /ping. Pick one and configure your infrastructure to use it. If you need both liveness and readiness checks, use /healthz for liveness and /ready for readiness.
How often should health checks run?
Every 10-30 seconds is standard. More frequent checks (every 5 seconds) detect failures faster but add load. Less frequent checks (every 60 seconds) mean failures go undetected longer. For most apps, 15-30 second intervals balance detection speed with overhead.
What should the health check return?
At minimum: HTTP 200 with { "status": "ok" } when healthy, HTTP 503 when unhealthy. Better: include uptime, timestamp, and per-dependency status with latency. Do not include secrets, connection strings, or stack traces in the response.
Do I need a health endpoint for a static site?
For a pure static site (HTML/CSS/JS served by Nginx or a CDN), a health endpoint is less critical because there are no application-level dependencies to check. But if you are using Nginx, a simple location /health { return 200 'ok'; } enables monitoring tools to verify the web server itself is running.