Endpoint de Health Check
Cómo implementar un endpoint de health check listo para producción para monitoreo y load balancers.
Visión General
Un endpoint de health check informa a load balancers, orquestadores (Kubernetes) y herramientas de monitoreo si tu aplicación está viva y lista para recibir tráfico. Un simple return 200 no es suficiente — los health checks de producción deben verificar el estado de dependencias críticas (base de datos, caché, cola de mensajes) para evitar servir requests cuando la app no puede cumplirlos.
Cuándo Usar
Usa este recurso cuando:
- Desplegues en Kubernetes (livenessProbe / readinessProbe). Consulta Docker Basics para fundamentos de contenedores.
- Ejecutes detrás de un load balancer o reverse proxy. Consulta Rate Limiting para protección de APIs.
- configures monitoreo y alertas (Prometheus, Datadog). Consulta Prometheus API Monitoring para colección de métricas.
- Necesites degradación graceful o lógica de circuit breaker. Consulta Circuit Breaker para aislamiento de fallas.
Solución
Python (FastAPI)
from fastapi import FastAPI, HTTPException
import psycopg2
import redis
app = FastAPI()
def check_database():
try:
conn = psycopg2.connect("dbname=test user=postgres")
conn.cursor().execute("SELECT 1")
conn.close()
return True
except Exception:
return False
def check_cache():
try:
r = redis.Redis(host="localhost", port=6379)
r.ping()
return True
except Exception:
return False
@app.get("/health/live")
def liveness():
return {"status": "alive"}
@app.get("/health/ready")
def readiness():
db_ok = check_database()
cache_ok = check_cache()
if db_ok and cache_ok:
return {"status": "ready", "checks": {"db": True, "cache": True}}
raise HTTPException(status_code=503, detail="Not ready")
JavaScript (Express.js)
const express = require("express");
const { Pool } = require("pg");
const app = express();
const db = new Pool({ connectionString: process.env.DATABASE_URL });
app.get("/health/live", (req, res) => {
res.status(200).json({ status: "alive" });
});
app.get("/health/ready", async (req, res) => {
try {
await db.query("SELECT 1");
res.status(200).json({ status: "ready", checks: { db: true } });
} catch (err) {
res.status(503).json({ status: "not ready", checks: { db: false } });
}
});
Java (Spring Boot Actuator)
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
private final DataSource dataSource;
public DatabaseHealthIndicator(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Health health() {
try (Connection conn = dataSource.getConnection()) {
if (conn.isValid(1)) {
return Health.up().withDetail("database", "reachable").build();
}
} catch (Exception e) {
return Health.down().withException(e).build();
}
return Health.down().withDetail("database", "timeout").build();
}
}
Explicación
Los health checks siguen el modelo de dos probes popularizado por Kubernetes:
- Liveness probe (
/health/live): Responde “El proceso está corriendo?”. Si falla, el orquestador reinicia el contenedor. - Readiness probe (
/health/ready): Responde “La app puede servir tráfico?”. Si falla, el orquestador remueve el pod del load balancer del servicio.
La diferencia clave: un liveness probe fallido dispara un reinicio, mientras que un readiness probe fallido solo detiene el enrutamiento de tráfico. Nunca pongas verificaciones pesadas de dependencias en el liveness probe, o un parpadeo transitorio de la base de datos causará un loop de reinicios.
Variantes
| Tipo de Probe | Endpoint | Verifica | Acción al Fallar |
|---|---|---|---|
| Liveness | /health/live | Proceso está corriendo | Reiniciar contenedor |
| Readiness | /health/ready | DB, caché, cola alcanzables | Remover del load balancer |
| Startup | /health/startup | App terminó inicialización | Esperar antes de otros probes |
| Deep | /health/deep | Test end-to-end completo | Alertar, page on-call |
Lo que funciona
- Separa liveness y readiness: Tienen propósitos diferentes y no deben compartir el mismo endpoint.
- Mantén liveness ligero: Solo verifica que el proceso no se haya bloqueado (deadlock).
- Timeout en verificaciones de dependencias: Una base de datos lenta no debe colgar tu health check indefinidamente.
- Retorna JSON estructurado: Incluye estado por check para que operadores diagnostiquen rápido.
- Loguea fallos de health check: Pero a un nivel de log bajo para evitar ruido.
Errores Comunes
- Un solo endpoint para todo: Causa reinicios innecesarios cuando una dependencia está temporalmente no disponible.
- Sin timeout en checks de BD: El health check se convierte en un vector de denegación de servicio.
- Cachear resultados de health por mucho tiempo: Un cache de 60 segundos anula el propósito del monitoreo en tiempo real.
- Retornar 500 para readiness: Algunos load balancers solo verifican 200; usa 503 para indisponibilidad intencional.
- Ignorar startup probes: Apps con inicialización larga son eliminadas por el liveness probe antes de terminar de iniciar.
Tips de Rendimiento
- Mantén los liveness probes bajo 1ms. El liveness probe solo debería verificar el estado del proceso — sin I/O, sin llamadas de red:
@app.get("/health/live")
def liveness():
return {"status": "alive"} # Respuesta instantánea
- Cachea resultados de readiness por 5-10 segundos. Evita golpear dependencias en cada verificación de probe:
from functools import lru_cache
import time
_readiness_cache = {"result": None, "ts": 0}
@app.get("/health/ready")
def readiness():
now = time.time()
if _readiness_cache["result"] and now - _readiness_cache["ts"] < 5:
return _readiness_cache["result"]
# Realizar verificaciones...
- Usa connection pooling para verificaciones de DB. Abrir una nueva conexión en cada health check es costoso:
# Reutilizar el connection pool de la app
db_pool = psycopg2.pool.SimpleConnectionPool(1, 5, dsn=DATABASE_URL)
def check_database():
conn = db_pool.getconn()
try:
cursor = conn.cursor()
cursor.execute("SELECT 1")
cursor.close()
return True
finally:
db_pool.putconn(conn)
- Paraleliza verificaciones de dependencias. Verifica múltiples dependencias concurrentemente:
import concurrent.futures
def readiness():
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
db_future = executor.submit(check_database)
cache_future = executor.submit(check_cache)
queue_future = executor.submit(check_message_queue)
checks = {
"db": db_future.result(timeout=2),
"cache": cache_future.result(timeout=2),
"queue": queue_future.result(timeout=2),
}
return checks
- Usa TCP probes para verificaciones muy ligeras. Si el overhead de parsing HTTP importa, usa un probe de TCP socket:
livenessProbe:
tcpSocket:
port: 8080
periodSeconds: 10 Preguntas frecuentes
Qué código HTTP debería retornar un readiness probe fallido?
Retorna 503 Service Unavailable. Esto señala a load balancers y orquestadores que la instancia no debe recibir tráfico, sin disparar alertas de error.
Debería verificar APIs externas en mi health check?
Solo si esas APIs son críticas para servir requests. Para dependencias no críticas, considera un estado "degraded" en lugar de "down". Nunca verifiques más de 3-4 dependencias para mantener el endpoint rápido.
Con qué frecuencia debería Kubernetes sondear mi health endpoint?
El default es cada 10 segundos, pero ajusta según tu SLA. Para liveness, 10s es típico. Para readiness, 5s si necesitas failover rápido. Siempre configura failureThreshold (3) y timeoutSeconds (1-3) para evitar flapping.
Go (net/http) Health Check
package main
import (
"context"
"database/sql"
"encoding/json"
"net/http"
"time"
_ "github.com/lib/pq"
"github.com/redis/go-redis/v9"
)
type HealthStatus struct {
Status string `json:"status"`
Checks map[string]bool `json:"checks"`
}
var db *sql.DB
var rdb *redis.Client
func checkDB() bool {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
return db.PingContext(ctx) == nil
}
func checkRedis() bool {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
return rdb.Ping(ctx).Err() == nil
}
func livenessHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(HealthStatus{Status: "alive", Checks: nil})
}
func readinessHandler(w http.ResponseWriter, r *http.Request) {
checks := map[string]bool{
"db": checkDB(),
"redis": checkRedis(),
}
allOK := true
for _, ok := range checks {
if !ok {
allOK = false
}
}
w.Header().Set("Content-Type", "application/json")
if allOK {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusServiceUnavailable)
}
json.NewEncoder(w).Encode(HealthStatus{Status: map[bool]string{true: "ready", false: "not ready"}[allOK], Checks: checks})
}
func main() {
http.HandleFunc("/health/live", livenessHandler)
http.HandleFunc("/health/ready", readinessHandler)
http.ListenAndServe(":8080", nil)
}
Configuración de Probes en Kubernetes
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
spec:
replicas: 3
template:
spec:
containers:
- name: api
image: myapp:v1.2.0
ports:
- containerPort: 8080
# Liveness: reiniciar si el proceso se bloquea
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
# Readiness: detener tráfico si las dependencias fallan
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
# Startup: esperar a que la app termine de inicializar
startupProbe:
httpGet:
path: /health/startup
port: 8080
initialDelaySeconds: 0
periodSeconds: 5
failureThreshold: 30 # Hasta 150s para startup
Health Check Compuesto con Circuit Breaker
import time
from dataclasses import dataclass, field
from enum import Enum
class State(Enum):
CLOSED = "closed"
OPEN = "open"
HALF_OPEN = "half_open"
@dataclass
class CircuitBreaker:
failure_threshold: int = 5
recovery_timeout: int = 30
state: State = State.CLOSED
failure_count: int = 0
last_failure_time: float = field(default=0)
def call(self, check_func):
if self.state == State.OPEN:
if time.time() - self.last_failure_time > self.recovery_timeout:
self.state = State.HALF_OPEN
else:
return False
try:
result = check_func()
if result:
self._on_success()
else:
self._on_failure()
return result
except Exception:
self._on_failure()
return False
def _on_success(self):
self.failure_count = 0
self.state = State.CLOSED
def _on_failure(self):
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = State.OPEN
# Uso en health check
db_breaker = CircuitBreaker(failure_threshold=3, recovery_timeout=30)
def check_database_with_breaker():
return db_breaker.call(check_database)
Health Check con Timeout y Caching
const express = require("express");
const { Pool } = require("pg");
const app = express();
const db = new Pool({ connectionString: process.env.DATABASE_URL });
// Cachear resultado de readiness por 5 segundos
let readinessCache = { result: null, timestamp: 0 };
const READINESS_CACHE_TTL = 5000;
async function checkDB() {
const client = await db.connect();
try {
await Promise.race([
client.query("SELECT 1"),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("DB timeout")), 2000)
),
]);
return true;
} catch {
return false;
} finally {
client.release();
}
}
app.get("/health/ready", async (req, res) => {
const now = Date.now();
if (readinessCache.result !== null && now - readinessCache.timestamp < READINESS_CACHE_TTL) {
const status = readinessCache.result ? 200 : 503;
return res.status(status).json({
status: readinessCache.result ? "ready" : "not ready",
cached: true,
checks: { db: readinessCache.result },
});
}
const dbOk = await checkDB();
readinessCache = { result: dbOk, timestamp: now };
const status = dbOk ? 200 : 503;
res.status(status).json({
status: dbOk ? "ready" : "not ready",
cached: false,
checks: { db: dbOk },
});
});
Integración de Health Check en Docker Compose
version: "3.9"
services:
app:
build: .
ports:
- "3000:3000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health/ready"]
interval: 10s
timeout: 3s
retries: 3
start_period: 15s
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 2s
retries: 5
Recursos Relacionados
Variables de Entorno
Cómo leer, establecer y gestionar variables de entorno de forma segura en Python, JavaScript y Java.
RecipeTareas programadas con Cron
Cómo programar y gestionar tareas recurrentes usando sintaxis cron en Linux, Python y Node.js.
PatternPatrón Abstract Factory
Crea familias de objetos relacionados sin especificar sus clases concretas. Patrón de diseño creacional para familias de objetos consistentes.
PatternPatrón Adapter
Convierte la interfaz de una clase en otra interfaz que los clientes esperan. Patrón de diseño estructural para compatibilidad de interfaces.
PatternPatrón Builder
Construye objetos complejos paso a paso. Patrón de diseño creacional para construcción de objetos legible y configurable.
RecipeTareas en Segundo Plano (Background Jobs)
Cómo programar y ejecutar tareas en segundo plano usando cron, colas de trabajo y workers.