Un ping a la página principal te dice si el servidor contesta. No te dice si la base de datos responde, si la cola se está llenando o si el proveedor de pagos lleva diez minutos dando errores. Para eso existen los endpoints de salud: una URL en la que el propio servicio comprueba sus dependencias y cuenta cómo está cada una. El problema es que no hay un único formato. Un equipo de Java usa Spring Boot Actuator, uno de .NET usa Health Checks de ASP.NET Core, otro sigue el borrador health+json del IETF y la plataforma corre en Kubernetes, que tiene su propio estilo. En este artículo te enseñamos cómo es cada uno, dónde suele fallar la configuración y cómo AAA Sentinel los lee sin pedirte que cambies nada.
Cuatro dialectos para la misma pregunta
Todos responden a «¿estás bien?», pero con palabras distintas:
- health+json: un borrador del IETF (draft-inadarei-api-health-check) con tipo de contenido application/health+json y tres estados: pass, warn y fail.
- Spring Boot Actuator: el endpoint /actuator/health, con los estados UP, DOWN, OUT_OF_SERVICE y UNKNOWN.
- ASP.NET Core Health Checks: lo que publica MapHealthChecks, con los estados Healthy, Degraded y Unhealthy.
- Kubernetes: /livez y /readyz del API server, que responden ok o, con ?verbose, una línea por comprobación.
health+json: el borrador del IETF
Es el formato más neutral: no depende de ningún framework. El documento lleva un status general y, opcionalmente, un objeto checks. Cada clave de checks nombra un componente y lo que se mide, con la forma componente:medida, y su valor es una lista, porque el mismo componente puede medirse en varios nodos. Cada entrada lleva su propio status y, si quieres, observedValue, observedUnit, componentType, componentId, time y output.
HTTP/1.1 200 OK
Content-Type: application/health+json
Cache-Control: no-store
{
"status": "warn",
"version": "1.4.2",
"checks": {
"postgres:responseTime": [
{
"componentType": "datastore",
"observedValue": 12,
"observedUnit": "ms",
"status": "pass",
"time": "2026-10-18T09:00:00Z"
}
],
"orders-queue:depth": [
{
"componentType": "component",
"observedValue": 1480,
"status": "warn",
"time": "2026-10-18T09:00:00Z"
}
]
}
}- pass: sano. warn: funciona, pero con algún problema. fail: no funciona.
- El borrador admite sinónimos: ok y up equivalen a pass; error y down equivalen a fail.
- El código HTTP acompaña al estado: 2xx para pass y warn, 4xx o 5xx para fail (lo habitual es 503).
- Campos opcionales a nivel de servicio: version, releaseId, serviceId, description, notes y output.
Spring Boot Actuator: /actuator/health
Con spring-boot-starter-actuator en el proyecto, /actuator/health está publicado por HTTP desde el primer arranque. Lo que no está es el detalle: por defecto, show-details vale never y la respuesta se queda en el estado agregado.
GET /actuator/health
HTTP/1.1 200 OK
{ "status": "UP" }GET /actuator/health
HTTP/1.1 503 Service Unavailable
{
"status": "DOWN",
"components": {
"db": {
"status": "DOWN",
"details": { "error": "org.springframework.jdbc.CannotGetJdbcConnectionException" }
},
"diskSpace": {
"status": "UP",
"details": { "total": 53687091200, "free": 31138512896, "threshold": 10485760, "exists": true }
},
"ping": { "status": "UP" }
}
}Desde Spring Boot 2.2 los indicadores van en components; antes iban en details. Por defecto, DOWN y OUT_OF_SERVICE responden 503, y UP y UNKNOWN responden 200. Ese mapeo se cambia con management.endpoint.health.status.http-mapping.
Cómo enseñar los componentes en Spring
Hay dos propiedades distintas y conviene no mezclarlas. show-components decide si aparece la lista de componentes con su estado; show-details decide si, además, aparece el detalle de cada uno (versión de la base de datos, espacio en disco, mensajes de error). Si no fijas show-components, toma el valor de show-details. Para un monitor externo basta con los estados, así que una configuración razonable es esta:
management:
endpoints:
web:
exposure:
include: health
endpoint:
health:
show-components: always
show-details: when-authorized
probes:
enabled: true- show-components: always enseña el estado de db, diskSpace, redis y el resto a cualquiera, sin datos internos.
- show-details: when-authorized reserva el detalle a usuarios autenticados (y, si fijas management.endpoint.health.roles, a esos roles).
- probes.enabled publica además /actuator/health/liveness y /actuator/health/readiness; en Kubernetes se activan solas.
- Si prefieres no exponer nada en el puerto público, management.server.port saca Actuator a otro puerto.
ASP.NET Core Health Checks
En .NET registras las comprobaciones con AddHealthChecks y publicas el endpoint con MapHealthChecks. Cada comprobación devuelve un HealthCheckResult, y el informe agregado toma el peor estado.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks()
.AddCheck<DatabaseHealthCheck>("database")
.AddCheck<QueueHealthCheck>("queue");
var app = builder.Build();
app.MapHealthChecks("/health");
app.Run();GET /health
HTTP/1.1 200 OK
Content-Type: text/plain
DegradedEl escritor por defecto devuelve solo una palabra en texto plano: Healthy, Degraded o Unhealthy. No hay componentes. Y por defecto Degraded responde 200, igual que Healthy; solo Unhealthy responde 503. Un monitor que mire únicamente el código HTTP nunca verá un servicio degradado. Los códigos se cambian con HealthCheckOptions.ResultStatusCodes.
Un escritor JSON para ASP.NET
Para tener el estado por componente necesitas un ResponseWriter. La opción sin dependencias es escribir el tuyo, al estilo del ejemplo de la documentación de Microsoft, con una propiedad results:
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = (context, report) => context.Response.WriteAsJsonAsync(new
{
status = report.Status.ToString(),
results = report.Entries.ToDictionary(
entry => entry.Key,
entry => new
{
status = entry.Value.Status.ToString(),
description = entry.Value.Description,
}),
}),
});{
"status": "Degraded",
"results": {
"database": { "status": "Healthy", "description": null },
"queue": { "status": "Degraded", "description": "1480 messages waiting" }
}
}- Serializa el estado con ToString(). Si serializas el enum HealthStatus tal cual, sin JsonStringEnumConverter, sale como número: 0 es Unhealthy, 1 es Degraded y 2 es Healthy.
- No pongas en la respuesta excepciones ni cadenas de conexión: los endpoints de salud suelen ser públicos.
La alternativa de la comunidad: HealthChecks.UI.Client
Muchos proyectos usan AspNetCore.HealthChecks.UI.Client, del proyecto comunitario AspNetCore.Diagnostics.HealthChecks (no es un paquete de Microsoft). Su escritor devuelve las comprobaciones en una propiedad entries, con duración y etiquetas:
// dotnet add package AspNetCore.HealthChecks.UI.Client
using HealthChecks.UI.Client;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
});{
"status": "Degraded",
"totalDuration": "00:00:00.0153210",
"entries": {
"database": { "data": {}, "duration": "00:00:00.0081120", "status": "Healthy", "tags": [] },
"queue": { "data": {}, "description": "1480 messages waiting", "duration": "00:00:00.0040310", "status": "Degraded", "tags": [] }
}
}Kubernetes: /livez y /readyz
El API server de Kubernetes publica /livez y /readyz (y el antiguo /healthz). Sin parámetros responden ok con un 200. Con ?verbose devuelven una línea por comprobación: [+]nombre ok si pasa y [-]nombre failed si no, con un 500 cuando alguna falla. Algunas aplicaciones imitan ese formato en sus propios endpoints.
GET /readyz?verbose
HTTP/1.1 500 Internal Server Error
[+]ping ok
[+]log ok
[-]etcd failed: reason withheld
[+]informer-sync ok
readyz check failedCómo se corresponden los estados
Los cuatro formatos tienen un estado sano y uno caído. El matiz está en el medio: warn, Degraded y UNKNOWN no son una caída. Así los lee AAA Sentinel:
Contrato de salud · de cada estado a sano, degradado o caído
- health+json
- pass, ok, up → sano · warn → degradado · fail, error, down → caído
- Spring Boot Actuator
- UP → sano · UNKNOWN → degradado · DOWN, OUT_OF_SERVICE → caído
- ASP.NET Core
- Healthy (o 2) → sano · Degraded (o 1) → degradado · Unhealthy (o 0) → caído
- Kubernetes
- ok o [+]nombre → sano · [-]nombre → caído
- Código HTTP fuera de 2xx
- Caído, diga lo que diga el cuerpo
- Cuerpo que no es ningún contrato
- Decide el código HTTP, como un ping; si el cuerpo llegó cortado, degradado
Las trampas de siempre
Casi todos los falsos «todo bien» vienen de aquí:
- Actuator sin show-components ni show-details: solo hay {"status":"UP"}, y un componente caído aparece únicamente como DOWN global, sin saber cuál.
- ASP.NET con el escritor por defecto: una palabra en texto plano y un 200 para Degraded.
- El código HTTP y el cuerpo se contradicen: un 200 con status fail (escritores propios que siempre responden 200) o un 503 con status pass (un proxy delante que devuelve su propio error).
- Degradado no es caído: si un warn despierta a alguien a las tres de la mañana, el equipo acabará silenciando el monitor.
- Las mayúsculas importan: up es health+json y UP es Actuator. Un Up a medias no es ninguno de los dos.
- Un endpoint de salud que tarda 30 segundos porque espera a una dependencia colgada. Pon un tiempo límite a cada comprobación.
Cómo lo lee el contrato de salud de AAA Sentinel
El monitor de tipo contrato de salud hace un GET a tu URL HTTPS y lee la respuesta tal como viene. No eliges el formato al crearlo: lo reconoce por el cuerpo. Estas son las reglas:
- Si el cuerpo es JSON, prueba en este orden: health+json (status en minúsculas, con sus sinónimos), Actuator (status en mayúsculas, componentes en components o en details) y ASP.NET (Healthy, Degraded o Unhealthy, con componentes en results o en entries).
- Un status numérico solo se lee como el enum de .NET si viene con sus entradas en results o entries; un número suelto podría ser de cualquiera.
- Si no es JSON, lee el texto plano del escritor por defecto de ASP.NET y, si no, la salida de Kubernetes: ok o una línea [+]/[-] por comprobación.
- De cada componente saca su estado. En health+json, si una clave trae varias entradas (varios nodos), cuenta la peor.
- El estado del servicio es el peor entre el que declara y el de sus componentes: un pass con un componente en fail es caído.
- Si el código HTTP no es 2xx, es caído aunque el cuerpo diga pass. Si es 2xx y el cuerpo dice fail, también es caído: justo el caso que un ping no ve.
- Si el cuerpo no encaja con ningún contrato, decide el código HTTP. Si llegó cortado por tamaño, no se da por sano: queda degradado.
- Puedes fijar una latencia máxima (hasta 10 segundos): si la respuesta tarda más, cuenta como caído.
Degradado y caído no se tratan igual
Un servicio degradado sigue sirviendo, así que no se trata como una caída:
- Degradado (un warn, un Degraded o un UNKNOWN): queda registrado y la página de estado lo muestra como degradado, pero no abre incidente ni salta como una caída.
- Caído: entra en la máquina de estados de siempre. Primero es sospechoso, la comprobación se repite desde una segunda región de AWS (eu-central-1) y, solo si se confirma, abre un incidente y avisa por correo, Slack, Microsoft Teams o webhook.
- Cuando vuelve a estar sano, el incidente se cierra solo y te llega la recuperación.
- Durante las primeras 24 horas el monitor observa y propone umbrales sin avisar.
Si tu endpoint pide credenciales, el monitor admite bearer, basic, API key, AWS SigV4 y tokens de Azure AD o Google Cloud con la identidad de la cuenta ya conectada. Cada sonda lleva la cabecera X-AAA-Signature, firmada con un secreto de tu organización, para que puedas distinguirla del resto del tráfico.
health+json en Node con @aaa-sentinel/health
Si tu servicio es Node y aún no tiene endpoint de salud, @aaa-sentinel/health (en npm, sin dependencias, Node 20 o posterior) te lo da hecho en health+json. Le pasas un objeto con una función por componente y te devuelve la respuesta HTTP completa. En una Lambda detrás de API Gateway o de una function URL, la devuelves tal cual:
import { runHealthChecks } from '@aaa-sentinel/health'
export const handler = () =>
runHealthChecks(
{
database: async () => {
await db.query('select 1')
},
queue: async () => ((await queueDepth()) < 1000 ? 'pass' : 'warn'),
payments: async (signal) => {
const started = Date.now()
await fetch('https://payments.internal/ping', { signal })
return { observedValue: Date.now() - started, observedUnit: 'ms' }
},
},
{ version: process.env.APP_VERSION, timeoutMs: 2000 },
)import express from 'express'
import { runHealthChecks } from '@aaa-sentinel/health'
const app = express()
app.get('/health', async (_req, res) => {
const r = await runHealthChecks(checks, { version: process.env.APP_VERSION })
res.writeHead(r.statusCode, r.headers).end(r.body)
})HTTP/1.1 200 OK
Content-Type: application/health+json
Cache-Control: no-store
{
"status": "warn",
"version": "1.4.2",
"checks": {
"database": [{ "status": "pass", "time": "2026-10-18T09:00:00.120Z" }],
"queue": [{ "status": "warn", "time": "2026-10-18T09:00:00.118Z" }],
"payments": [{ "observedValue": 41, "observedUnit": "ms", "status": "pass", "time": "2026-10-18T09:00:00.161Z" }]
}
}- Una comprobación que no devuelve nada o devuelve true es pass; false es fail; también puede devolver pass, warn o fail, o un objeto con observedValue y observedUnit.
- Las comprobaciones corren a la vez, cada una con su tiempo límite (timeoutMs, 2 segundos por defecto y 60 como máximo). La que lanza un error o se pasa de tiempo es fail para ese componente, y el endpoint sigue respondiendo. Recibe un AbortSignal para cortar la llamada.
- El estado general es la peor comprobación. fail responde 503; pass y warn, 200. Así también lo entienden los balanceadores.
- Los mensajes de error no salen en la respuesta salvo que pases exposeErrors: true, porque suelen contener hosts o cadenas de conexión.
- minimumStatus: 'warn' deja el servicio como mínimo en warn, por ejemplo durante un mantenimiento.
- Si ya tienes los resultados, healthResponse construye la misma respuesta sin ejecutar nada, y healthBody te da solo el documento.
Checklist
Antes de crear el monitor, comprueba que tu endpoint cumple esto:
- Responde por HTTPS a un GET, sin redirigir a una página de login.
- Devuelve el estado de cada componente: show-components en Spring, un ResponseWriter JSON en ASP.NET, checks en health+json.
- No expone detalles internos a quien no debe verlos.
- Cada comprobación tiene un tiempo límite y el endpoint responde en pocos segundos.
- Distingue degradado de caído: warn, Degraded o UNKNOWN para lo que funciona a medias.
- El código HTTP es coherente con el cuerpo: 503 cuando el estado es caído.
- Si es Node, @aaa-sentinel/health te da el formato, los tiempos límite y el código HTTP en unas líneas.