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 (Docker, Kubernetes) use health checks to restart crashed containers
- 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. According to Datadog's 2024 Container Report, containers without health checks have 3.2x longer mean time to recovery (MTTR) because failures go undetected until users report them. The Uptime Institute's 2024 report found that the average cost of a data center outage is $5,600 per minute - and detection time is the largest variable.
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
app.get('/health', async (req, res) => {
const checks = {};
const start = Date.now();
// Check database
try {
const dbStart = Date.now();
await 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 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
Kubernetes and Docker Compose distinguish between two types of health checks, 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:
// 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
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.
Tools like VibeDoctor (vibedoctor.io) automatically check whether your application exposes a health endpoint and flag the absence as a configuration issue. 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.