No Health Endpoint, No Uptime: Why Your App Needs /health - VibeDoctor 
← All Articles ⚙️ Configuration & DevOps Medium

No Health Endpoint, No Uptime: Why Your App Needs /health

AI-generated apps ship without a /health endpoint. Without one, your monitoring, load balancer, and container orchestrator are flying blind.

CFG-008

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:

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

  1. Checking auth on the health endpoint. Health checks must be unauthenticated. Your load balancer does not have a JWT token. Keep /health public but do not expose sensitive internal details.
  2. 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 1 or PING, not a full table scan.
  3. 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).
  4. 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.

Scan your codebase for this issue - free

VibeDoctor checks for CFG-008 and 128 other issues across 15 diagnostic areas.

SCAN MY APP →
← Back to all articles View all 129+ checks →