Logby

Como Configurar Health Checks em ASP.NET Core

Por Tiago

Quando colocamos uma aplicação em produção, especialmente em ambientes distribuídos com containers e orquestradores como Kubernetes, surge uma pergunta recorrente: como saber, de forma automatizada, se a aplicação está realmente funcionando? Não basta o processo estar de pé — é preciso garantir que ela consegue responder requisições e que suas dependências (banco de dados, filas, APIs externas) estão saudáveis. É exatamente esse problema que os health checks do ASP.NET Core resolvem.

Neste post, vamos entender o que são health checks, como configurá-los do zero em uma aplicação ASP.NET Core, como criar verificações customizadas para dependências específicas e como integrar tudo isso com ferramentas de orquestração como Docker.

O que são health checks e por que usá-los

Health checks são endpoints HTTP expostos pela própria aplicação que informam seu estado de saúde. Esses endpoints são consumidos, geralmente, por sistemas externos de monitoramento, load balancers e orquestradores de containers — não por usuários finais navegando no sistema.

Existem alguns cenários típicos de uso:

  • Sondas de liveness/readiness em orquestradores: ferramentas como Kubernetes usam esses endpoints para decidir se um container deve continuar recebendo tráfego, ser reiniciado, ou se um deploy em andamento deve ser interrompido.
  • Roteamento de tráfego em load balancers: se uma instância reporta estado não saudável, o balanceador para de enviar requisições para ela, direcionando o tráfego para réplicas saudáveis.
  • Monitoramento de recursos físicos: uso de memória, disco e outros recursos do servidor podem ser incluídos na verificação de saúde.
  • Verificação de dependências externas: bancos de dados, APIs de terceiros, filas de mensagens — tudo que a aplicação precisa para funcionar corretamente pode (e deve) ser testado.

Antes de sair adicionando health checks em todo lugar, vale a pena pensar em qual sistema de monitoramento ou orquestração vai consumir essas informações. Isso influencia diretamente que tipos de verificação fazem sentido e como os endpoints devem ser estruturados.

Configuração básica

Para a maioria das aplicações, uma configuração simples que apenas informa se a aplicação está disponível para processar requisições (o que costuma ser chamado de liveness) já é suficiente.

A configuração básica envolve dois passos: registrar o serviço de health checks no container de injeção de dependência e mapear um endpoint HTTP que responde com o status de saúde.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHealthChecks();

var app = builder.Build();

app.MapHealthChecks("/healthz");

app.Run();

Com isso, ao acessar /healthz, a aplicação retorna um texto simples representando o status atual, que pode ser:

  • Healthy — tudo funcionando normalmente;
  • Degraded — a aplicação está operando, mas com alguma limitação ou lentidão;
  • Unhealthy — algo está impedindo o funcionamento correto.

Nessa configuração mínima, nenhuma verificação específica de dependência está registrada — o middleware apenas confirma que a aplicação consegue responder à requisição naquele endpoint. Isso já é útil como sonda de liveness básica, mas na prática a maioria dos sistemas reais precisa de verificações mais específicas.

Adicionando verificações de dependências

O poder real dos health checks aparece quando combinamos essa infraestrutura com verificações específicas de cada dependência crítica da aplicação. É possível, por exemplo, verificar se o banco de dados está acessível, se uma fila de mensagens responde, ou se uma API externa está operacional.

Uma abordagem comum é implementar a interface IHealthCheck, que expõe um método CheckHealthAsync. Esse método deve retornar um HealthCheckResult indicando se aquela dependência específica está saudável, degradada ou indisponível.

public class BancoDeDadosHealthCheck : IHealthCheck
{
    private readonly MeuDbContext _contexto;

    public BancoDeDadosHealthCheck(MeuDbContext contexto)
    {
        _contexto = contexto;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            await _contexto.Database.CanConnectAsync(cancellationToken);
            return HealthCheckResult.Healthy("Conexão com o banco OK.");
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy("Falha ao conectar ao banco.", ex);
        }
    }
}

Em seguida, basta registrar essa verificação junto com as demais:

builder.Services.AddHealthChecks()
    .AddCheck<BancoDeDadosHealthCheck>("banco_de_dados");

Vale mencionar também que existe um ecossistema de pacotes comunitários (como AspNetCore.HealthChecks.SqlServer, AspNetCore.HealthChecks.Redis, entre outros) que já implementam verificações prontas para bancos de dados populares, sistemas de cache, mensageria e serviços de nuvem, evitando que você precise escrever tudo do zero.

Separando liveness e readiness com tags

Em ambientes orquestrados, é comum distinguir entre dois tipos de sonda:

  • Liveness: responde se o processo da aplicação ainda está "vivo" — se estiver travado, o orquestrador pode reiniciar o container.
  • Readiness: responde se a aplicação está pronta para receber tráfego — mesmo viva, ela pode estar temporariamente indisponível por conta de uma dependência fora do ar.

Para diferenciar esses dois cenários no mesmo projeto, uma técnica útil é usar tags nos health checks e filtrar quais verificações cada endpoint deve considerar:

builder.Services.AddHealthChecks()
    .AddCheck<BancoDeDadosHealthCheck>("banco_de_dados", tags: new[] { "ready" });

app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false // não executa nenhum check específico, só confirma que o processo responde
});

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready")
});

Dessa forma, o endpoint /health/live serve como sonda de liveness simples (o processo responde), enquanto /health/ready executa de fato as verificações de dependências antes de declarar a aplicação pronta para receber tráfego.

Integrando com Docker

Containers Docker suportam uma diretiva nativa chamada HEALTHCHECK, que permite ao próprio Docker consultar periodicamente o endpoint de saúde da aplicação e reportar o status do container.

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD curl --fail http://localhost:5000/healthz || exit 1

Com essa configuração, ferramentas como docker ps já mostram se o container está healthy ou unhealthy, e orquestradores que leem esse status podem tomar decisões automáticas, como reiniciar o container ou removê-lo de um pool de balanceamento.

Boas práticas

Algumas recomendações práticas para quando você for implementar health checks em projetos reais:

  • Não exponha detalhes sensíveis nas respostas dos health checks públicos — evite vazar mensagens de exceção completas ou strings de conexão em ambientes acessíveis externamente.
  • Separe liveness de readiness sempre que a aplicação tiver dependências externas, para evitar reinícios desnecessários de containers quando o problema é, na verdade, em um serviço terceiro.
  • Defina timeouts curtos nas verificações de dependências, para que uma dependência lenta não trave a resposta do próprio endpoint de health check.
  • Use tags para agrupar verificações por criticidade ou por tipo de sonda, facilitando a filtragem em diferentes endpoints.

Conclusão

Health checks são uma peça pequena, mas fundamental, na construção de aplicações resilientes e prontas para operar em ambientes modernos de infraestrutura. O ASP.NET Core torna essa implementação bastante direta: em poucas linhas já temos um endpoint funcional, e a partir daí é possível evoluir para verificações customizadas de dependências, segmentação entre liveness e readiness, e integração nativa com Docker e orquestradores.

Investir tempo nessa configuração logo no início do projeto evita dores de cabeça em produção — principalmente aquele tipo de incidente em que a aplicação "parece" estar de pé, mas na prática não consegue atender ninguém porque uma dependência crítica caiu silenciosamente.

Fontes