Sua API está rodando, os testes passaram, o deploy foi pro cluster. Tudo certo? Não necessariamente. Se o Kubernetes não consegue determinar se o seu pod está saudável, ele pode derrubar containers funcionais ou — pior — continuar mandando tráfego para pods que já travaram. Health checks são a forma que o Kubernetes tem de perguntar ao seu app: “você está vivo? Está pronto pra trabalhar?”. Sem eles, você está voando às cegas em produção.
Neste post, vamos configurar health checks completos em uma API .NET 10 com Minimal API — desde o endpoint mais simples até a integração com PostgreSQL, Redis e o mapeamento nos probes do Kubernetes. Se você roda workloads no GKE Autopilot (como discutimos no nosso guia de GKE Autopilot), este post é o complemento direto.
O que são Health Checks no ASP.NET Core
O ASP.NET Core traz um middleware nativo de health checks via Microsoft.Extensions.Diagnostics.HealthChecks. A ideia é simples: você registra um ou mais checks que verificam a saúde da aplicação e expõe um endpoint HTTP que retorna o resultado.
Cada check pode retornar um de três estados:
- Healthy — tudo funcionando normalmente
- Degraded — o app funciona, mas algo está abaixo do esperado (ex: latência alta no banco)
- Unhealthy — algo crítico falhou (ex: banco inacessível)
A configuração mínima em uma Minimal API:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks();
var app = builder.Build();
app.MapHealthChecks("/health");
app.Run();
Ao acessar GET /health, a resposta é um 200 OK com o body Healthy em text/plain. Se qualquer check registrado retornar Unhealthy, o status code muda para 503 Service Unavailable. Simples, funcional, mas insuficiente para Kubernetes — você precisa de mais granularidade.
Separando Startup, Liveness e Readiness
No Kubernetes, um único endpoint /health não basta. O orquestrador precisa saber três coisas diferentes sobre o seu container:
Startup Probe
O container terminou de inicializar? Essa probe é verificada apenas no início do ciclo de vida do pod. Enquanto a startup probe não passar, o Kubernetes não começa a verificar liveness nem readiness. É útil para aplicações que demoram para inicializar — como uma API que precisa rodar migrations, popular cache ou conectar em múltiplos serviços.
Liveness Probe
O processo travou? Se o liveness check falha repetidamente, o Kubernetes reinicia o container. Aqui você não deve verificar dependências externas (banco, Redis). Se o banco caiu, reiniciar o pod não vai resolver nada. O liveness deve verificar apenas se o processo da aplicação está respondendo.
Readiness Probe
O app está pronto para receber tráfego? Diferente do liveness, o readiness deve verificar dependências. Se o banco está fora, o pod é removido do Service (para de receber requests) mas não é reiniciado. Quando a dependência volta, o pod volta a receber tráfego automaticamente.
Implementando com tags
A estratégia é usar tags para separar quais checks pertencem a qual probe:
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("PostgreSQL")!;
var redisConnectionString = builder.Configuration.GetConnectionString("Redis")!;
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: ["live"])
.AddNpgSql(connectionString, tags: ["ready"])
.AddRedis(redisConnectionString, tags: ["ready"]);
var app = builder.Build();
Agora, mapeamos três endpoints distintos, cada um filtrando por tag:
app.MapHealthChecks("/health/startup", new HealthCheckOptions
{
Predicate = _ => false // não executa nenhum check — só verifica se o app responde
});
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("live")
});
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready")
});
app.Run();
O /health/startup com Predicate = _ => false é um truque: ele não executa nenhum health check, apenas verifica se o pipeline HTTP do app está respondendo. Se o Kestrel responde, o container inicializou.
Integrando com PostgreSQL e Redis
Para que os checks de readiness realmente validem suas dependências, você precisa dos pacotes da comunidade Xabaril/AspNetCore.Diagnostics.HealthChecks:
dotnet add package AspNetCore.HealthChecks.NpgSql
dotnet add package AspNetCore.HealthChecks.Redis
A configuração completa no Program.cs:
var builder = WebApplication.CreateBuilder(args);
var pgConnection = builder.Configuration.GetConnectionString("PostgreSQL")!;
var redisConnection = builder.Configuration.GetConnectionString("Redis")!;
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: ["live"])
.AddNpgSql(
connectionString: pgConnection,
healthQuery: "SELECT 1;",
name: "postgresql",
failureStatus: HealthStatus.Unhealthy,
tags: ["ready"],
timeout: TimeSpan.FromSeconds(3))
.AddRedis(
redisConnectionString: redisConnection,
name: "redis",
failureStatus: HealthStatus.Unhealthy,
tags: ["ready"],
timeout: TimeSpan.FromSeconds(3));
O timeout é crítico. Sem ele, uma dependência lenta pode travar a probe por tempo indeterminado. Com 3 segundos, o check falha rápido e o Kubernetes toma a decisão certa.
O AddNpgSql executa a query SELECT 1; no PostgreSQL para validar que a conexão está funcional. Se você roda no GKE com Cloud SQL, esse check valida a conectividade real pelo Cloud SQL Auth Proxy — é o caminho completo, não só um ping na porta. Se a instância Cloud SQL estiver parada ou o proxy tiver caído, o readiness probe vai falhar e o pod sai do Service.
O AddRedis valida a conexão com o Redis usando PING. Se você usa Memorystore no Google Cloud, funciona da mesma forma.
Dica: se você usa Refit + Polly para chamadas HTTP resilientes, considere adicionar um health check para suas APIs externas críticas no readiness — assim o pod para de receber tráfego se uma dependência vital estiver fora.
Criando um Health Check customizado
Nem toda verificação tem um pacote NuGet pronto. Para cenários específicos, implemente IHealthCheck:
public class ExternalPaymentApiHealthCheck : IHealthCheck
{
private readonly HttpClient _httpClient;
public ExternalPaymentApiHealthCheck(IHttpClientFactory httpClientFactory)
{
_httpClient = httpClientFactory.CreateClient("PaymentApi");
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
var response = await _httpClient.GetAsync("/health", cancellationToken);
return response.IsSuccessStatusCode
? HealthCheckResult.Healthy("API de pagamento respondendo.")
: HealthCheckResult.Degraded($"API de pagamento retornou {response.StatusCode}.");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("API de pagamento inacessível.", ex);
}
}
}
Registre no container de DI:
builder.Services.AddHealthChecks()
.AddCheck<ExternalPaymentApiHealthCheck>(
"payment-api",
failureStatus: HealthStatus.Degraded,
tags: ["ready"]);
Quando criar um check customizado vs. usar pacote pronto? Se existe um pacote mantido do AspNetCore.Diagnostics.HealthChecks para o recurso (PostgreSQL, Redis, RabbitMQ, Azure Service Bus, etc.), use o pacote. Crie um check customizado para APIs internas da sua empresa, verificações de filesystem, validações de configuração ou qualquer recurso que não tenha pacote disponível.
Retornando JSON detalhado
O response padrão do health check é um text/plain com apenas “Healthy” ou “Unhealthy” — zero detalhes. Em produção, você precisa saber qual check falhou e quanto tempo levou. Configure um ResponseWriter customizado:
using System.Text.Json;
using Microsoft.Extensions.Diagnostics.HealthChecks;
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready"),
ResponseWriter = async (context, report) =>
{
context.Response.ContentType = "application/json";
var result = new
{
status = report.Status.ToString(),
totalDuration = report.TotalDuration.TotalMilliseconds + "ms",
checks = report.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString(),
duration = e.Value.Duration.TotalMilliseconds + "ms",
description = e.Value.Description,
exception = e.Value.Exception?.Message
})
};
await context.Response.WriteAsync(
JsonSerializer.Serialize(result, new JsonSerializerOptions
{
WriteIndented = true
}));
}
});
Agora o endpoint retorna algo como:
{
"status": "Healthy",
"totalDuration": "45ms",
"checks": [
{
"name": "postgresql",
"status": "Healthy",
"duration": "12ms",
"description": null,
"exception": null
},
{
"name": "redis",
"status": "Healthy",
"duration": "3ms",
"description": null,
"exception": null
}
]
}
Muito mais útil para debugging. Note que o endpoint de readiness do Kubernetes pode continuar usando a resposta simples — o JSON detalhado é mais útil em um endpoint /health/detail separado para consumo humano ou por dashboards.
Mapeando nos probes do Kubernetes
Agora que a API expõe /health/startup, /health/live e /health/ready, vamos mapear esses endpoints nos probes do Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: minha-api
spec:
replicas: 2
selector:
matchLabels:
app: minha-api
template:
metadata:
labels:
app: minha-api
spec:
containers:
- name: minha-api
image: gcr.io/meu-projeto/minha-api:latest
ports:
- containerPort: 8080
startupProbe:
httpGet:
path: /health/startup
port: 8080
initialDelaySeconds: 0
periodSeconds: 3
failureThreshold: 30
livenessProbe:
httpGet:
path: /health/live
port: 8080
periodSeconds: 15
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 8080
periodSeconds: 10
failureThreshold: 3
Entendendo os parâmetros
initialDelaySeconds— quanto tempo esperar antes da primeira verificação. No startup probe, use0porque o própriofailureThreshold * periodSecondsjá dá a janela necessária.periodSeconds— intervalo entre verificações. Liveness não precisa ser agressivo (15s é suficiente). Readiness pode ser mais frequente (10s) para reagir rápido a dependências que voltam.failureThreshold— quantas falhas consecutivas antes de agir. Com startup probe deperiodSeconds: 3efailureThreshold: 30, o app tem até 90 segundos para inicializar.timeoutSeconds— (default: 1s) timeout da requisição HTTP do probe. Se seu health check consulta banco, considere aumentar para3ou5.
Dicas para GKE Autopilot
Como abordamos no guia de GKE Autopilot, o Autopilot exige resources.requests definidos no container para que os probes funcionem corretamente. Sem requests, o Autopilot aplica defaults que podem não ser adequados para sua aplicação — e os probes podem falhar por falta de CPU durante a inicialização.
Armadilha comum: readiness agressivo durante deploy
Se seu readinessProbe tem periodSeconds: 2 e failureThreshold: 1, qualquer latência momentânea no banco durante um deploy (quando múltiplos pods reiniciam simultaneamente) vai derrubar todos os pods do Service. Use valores conservadores: periodSeconds: 10 e failureThreshold: 3 dão 30 segundos de margem antes de remover o pod do tráfego.
Outra armadilha: colocar checks pesados no liveness probe. Se a query do banco demora 5 segundos e o timeoutSeconds do probe é 1, o Kubernetes vai reiniciar o pod sem necessidade. Liveness deve ser leve — por isso usamos apenas o check "self" nele.
Bônus: Health Check UI (dashboard)
Para ambientes de desenvolvimento e staging, o pacote AspNetCore.HealthChecks.UI oferece um dashboard visual:
dotnet add package AspNetCore.HealthChecks.UI
dotnet add package AspNetCore.HealthChecks.UI.InMemory.Storage
builder.Services
.AddHealthChecksUI(options =>
{
options.SetEvaluationTimeInSeconds(30);
options.AddHealthCheckEndpoint("API", "/health/ready");
})
.AddInMemoryStorage();
app.MapHealthChecksUI(options => options.UIPath = "/health-ui");
Acesse /health-ui e você tem um painel com o status de cada check, histórico e gráficos. É excelente para visualizar rapidamente a saúde dos serviços em staging. Em produção, o ideal é exportar métricas via OpenTelemetry para Prometheus + Grafana ou usar o dashboard nativo do GKE no Cloud Console — ambos oferecem alertas, histórico longo e integração com o ecossistema de observabilidade.
Conclusão
Health checks não são um “nice to have” — são infraestrutura obrigatória quando você roda em Kubernetes. Sem probes configurados, o GKE não sabe se seu pod travou, se está pronto para receber tráfego ou se ainda está inicializando. O resultado são restarts desnecessários, downtime silencioso e usuários recebendo erros 502.
Com a configuração que montamos — startup, liveness e readiness separados, dependências validadas via AspNetCore.HealthChecks, timeout nos checks e probes mapeados no Deployment — sua API comunica ao Kubernetes exatamente o que ele precisa saber.
Como próximos passos, considere adicionar OpenTelemetry para exportar métricas de health check para o Cloud Monitoring e configurar alertas. Se você já otimizou seu Dockerfile para produção e configurou resiliência nas chamadas HTTP, health checks são a peça que faltava para uma operação sólida no GKE.
Na DevPlus, rodamos todas as nossas APIs com essa configuração. Se precisar de ajuda para implementar health checks ou montar a infraestrutura Kubernetes do seu projeto, fale com a gente.