ALB Retornando 502 Bad Gateway: Instâncias Saudáveis, Resposta Inválida

Você abre o console da AWS, confere o target group e todas as instâncias estão Healthy. Mesmo assim, o ALB continua retornando 502 Bad Gateway para os clientes. Esse é um dos cenários mais frustrantes em operações com Application Load Balancer — o health check passa, mas a aplicação real falha silenciosamente na camada de protocolo HTTP.

TL;DR — Diagnóstico Rápido do 502 no ALB

Causa RaizSinal ObservávelCamada
Resposta HTTP malformada da aplicação502 imediato, sem latênciaAplicação
Conexão fechada antes da resposta completa502 intermitente sob cargaKeep-alive / TCP
Timeout de idle no ALB menor que no backend502 em requisições longasConfiguração ALB
Header HTTP inválido ou tamanho excedido502 em rotas específicasProtocolo HTTP
Target desregistrado durante requisição ativa502 durante deploy/scale-inCiclo de vida

Como o ALB Processa Respostas — Antes de Depurar

O ALB opera na camada 7 e termina a conexão TCP/TLS com o cliente. Ele então abre uma conexão separada com o target registrado. Isso significa que o ALB valida ativamente a resposta HTTP que recebe do backend antes de repassá-la ao cliente. Se a resposta não for um HTTP válido — cabeçalho malformado, status line inválida, corpo truncado — o ALB descarta a resposta e retorna 502 ao cliente. O health check, por padrão, verifica apenas se o endpoint responde com um código HTTP esperado em um path específico. Ele não valida o comportamento da aplicação sob carga real, com headers reais, ou com payloads reais.

sequenceDiagram participant C as Cliente participant ALB as ALB participant HC as Health Check participant T as Target (EC2) HC->>T: GET /health (intervalo configurado) T-->>HC: 200 OK Note over HC,T: Target marcado como Healthy C->>ALB: GET /api/dados (requisição real) ALB->>T: GET /api/dados (nova conexão HTTP) T-->>ALB: Resposta HTTP malformada Note over ALB,T: ALB valida protocolo HTTP ALB-->>C: 502 Bad Gateway Note over C,ALB: Health check continua verde
  1. Cliente → ALB: Conexão TLS terminada no ALB. O cliente nunca fala diretamente com o backend.
  2. ALB → Target: Nova conexão HTTP/HTTPS estabelecida. O ALB age como proxy reverso.
  3. Validação da resposta: O ALB inspeciona a resposta HTTP. Qualquer violação de protocolo gera 502.
  4. Health Check (paralelo): Roda em intervalo configurado, independente do tráfego real. Um target pode passar no health check e ainda assim gerar 502 em requisições reais.
Pense no ALB como um inspetor alfandegário: ele verifica o passaporte (health check) na entrada, mas inspeciona a bagagem (resposta HTTP) em cada passagem. Passaporte válido não garante bagagem aprovada.

Diagnóstico do 502 no ALB: Passo a Passo

Passo 1 — Confirme que o 502 é originado no ALB, não na aplicação

Antes de qualquer coisa, você precisa saber se o 502 está sendo gerado pelo ALB ou se a própria aplicação está retornando 502 para o ALB (o que também resulta em 502 para o cliente). Os access logs do ALB registram o campo target-status-code separado do elb-status-code. Se target-status-code for vazio ou -, o ALB nunca recebeu uma resposta válida do backend — o problema está na camada de protocolo ou conexão, não na lógica da aplicação.

Habilite os access logs do ALB se ainda não estiver ativo:

aws elbv2 modify-load-balancer-attributes \
  --load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/meu-alb/1234567890abcdef \
  --attributes Key=access_logs.s3.enabled,Value=true \
               Key=access_logs.s3.bucket,Value=meu-bucket-logs \
               Key=access_logs.s3.prefix,Value=alb-logs

Após alguns minutos, consulte os logs no S3 e filtre por status 502:

aws s3 cp s3://meu-bucket-logs/alb-logs/ ./alb-logs/ --recursive --exclude "*" --include "*.log.gz"
zcat alb-logs/*.log.gz | awk '$9 == 502 {print $9, $13, $14}' | head -50

Os campos impressos são: elb-status-code, target-status-code e target-processing-time. Se target-status-code for -, pule para o Passo 3. Se for um código válido como 502, a aplicação está gerando o erro — investigue nos logs da aplicação.

Passo 2 — Verifique o timeout de idle do ALB versus o keep-alive do backend

Esse é o erro de diagnóstico mais comum que vejo em produção. O timeout de idle padrão do ALB é 60 segundos. Se o servidor de aplicação (Nginx, Node.js, Gunicorn, etc.) fechar a conexão keep-alive antes desse tempo, o ALB pode tentar reutilizar uma conexão já fechada pelo backend — e quando isso acontece no meio de uma requisição, o resultado é 502. O problema é intermitente e piora sob carga, o que leva equipes a culpar a aplicação ou a infraestrutura de rede.

Consulte o timeout atual do ALB:

aws elbv2 describe-load-balancer-attributes \
  --load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/meu-alb/1234567890abcdef \
  --query 'Attributes[?Key==`idle_timeout.timeout_seconds`]'

A regra operacional: configure o keepalive_timeout do servidor de aplicação para um valor maior que o idle timeout do ALB. Se o ALB está em 60s, configure o backend para 65s ou mais. Isso garante que o ALB sempre feche a conexão antes do backend.

Para ajustar o idle timeout do ALB:

aws elbv2 modify-load-balancer-attributes \
  --load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/meu-alb/1234567890abcdef \
  --attributes Key=idle_timeout.timeout_seconds,Value=60

Passo 3 — Inspecione a resposta HTTP diretamente do backend

O health check do ALB não reproduz uma requisição real. Para replicar o que o ALB enxerga, conecte-se diretamente à instância e faça uma requisição HTTP crua. Isso expõe headers malformados, status lines inválidas, ou respostas sem Content-Length e sem Transfer-Encoding — todas causas documentadas de 502 no ALB.

# Substitua 10.0.1.50 pelo IP privado da instância e 8080 pela porta do target group
curl -v --http1.1 -H 'Host: meu-dominio.com' http://10.0.1.50:8080/caminho-que-gera-502 2>&1 | head -60

Procure por:

  • Status line com formato inválido (ex: HTTP/1.1200 OK sem espaço)
  • Headers com caracteres de controle ou encoding inválido
  • Resposta sem Content-Length e sem Transfer-Encoding: chunked em conexões persistentes
  • Corpo da resposta truncado antes do fim declarado pelo Content-Length

Passo 4 — Verifique o protocolo configurado no target group

Se o target group está configurado para HTTPS mas a aplicação responde em HTTP (ou vice-versa), o ALB recebe dados que não consegue interpretar como HTTP válido e retorna 502. Esse erro é especialmente comum após migrações ou mudanças de configuração de TLS no backend.

aws elbv2 describe-target-groups \
  --target-group-arns arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/meu-tg/abcdef1234567890 \
  --query 'TargetGroups[0].{Protocol:Protocol,Port:Port,HealthCheckProtocol:HealthCheckProtocol}'

Confirme que o protocolo e a porta retornados correspondem ao que a aplicação realmente escuta. Um target group HTTPS apontando para uma aplicação que responde em HTTP vai gerar 502 consistente — e o health check pode estar usando HTTP em um path diferente, mascarando o problema.

Passo 5 — Identifique 502s durante deploys e scale-in

Targets sendo desregistrados durante requisições ativas geram 502. O ALB encaminha a requisição, o target começa a processar, e então a conexão é encerrada pelo processo de desregistro. O atributo deregistration_delay.timeout_seconds controla quanto tempo o ALB aguarda conexões ativas drenarem antes de remover o target definitivamente.

aws elbv2 describe-target-group-attributes \
  --target-group-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/meu-tg/abcdef1234567890 \
  --query 'Attributes[?Key==`deregistration_delay.timeout_seconds`]'

Se o valor estiver muito baixo (ex: 5s) e suas requisições levam mais tempo para completar, aumente para um valor que cubra o tempo máximo de processamento de uma requisição típica. O padrão é 300 segundos.

aws elbv2 modify-target-group-attributes \
  --target-group-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/meu-tg/abcdef1234567890 \
  --attributes Key=deregistration_delay.timeout_seconds,Value=30
graph TD A["502 Bad Gateway"] --> B{"target-status-code nos logs do ALB"}; B -- "-" --> C["ALB não recebeu resposta"]; B -- "código válido" --> D["Aplicação retornou erro"]; C --> E{"target-processing-time"}; E -- "-1" --> F["Conexão recusada ou fechada prematuramente"]; E -- "valor alto" --> G["Timeout de processamento ou idle timeout"]; F --> H["Verificar keep-alive do backend vs ALB"]; F --> I["Verificar protocolo do target group"]; G --> J["Ajustar idle_timeout no ALB"]; G --> K["Verificar deregistration_delay durante deploys"]; D --> L["Investigar logs da aplicação"];
  1. Resposta inválida: ALB recebe dados que violam o protocolo HTTP — 502 imediato.
  2. Conexão fechada prematuramente: Backend fecha keep-alive antes do ALB — 502 intermitente.
  3. Timeout de processamento: Requisição excede o idle timeout configurado — 502 em operações longas.
  4. Desregistro durante requisição: Target removido enquanto processa — 502 durante deploys.

O Diagnóstico que Nos Custou Duas Horas

Em um ambiente de produção com Node.js atrás de um ALB, começamos a ver 502s esporádicos sob carga moderada. Os logs da aplicação não mostravam nada — nenhum erro, nenhum crash, nenhum timeout. O health check estava verde. Métricas de CPU e memória normais.

A hipótese inicial foi Security Group bloqueando tráfego intermitentemente. Verificamos as regras, tudo correto. Depois suspeitamos de um bug na aplicação em uma rota específica. Não era — o 502 aparecia em rotas diferentes a cada vez.

Quando finalmente olhamos os access logs do ALB com atenção, o campo target-status-code estava vazio (-) em todos os 502s. O ALB nunca recebia resposta. O target-processing-time era exatamente -1 — conexão recusada ou fechada antes da resposta.

A causa: o servidor HTTP do Node.js tinha server.keepAliveTimeout configurado para 5 segundos (padrão em versões mais antigas do Node.js). O ALB, com idle timeout de 60 segundos, mantinha conexões abertas por até 60s e tentava reutilizá-las. Quando o Node.js fechava a conexão após 5s de idle e o ALB tentava enviar uma nova requisição por essa conexão já fechada, o resultado era 502.

A correção foi adicionar duas linhas no código de inicialização do servidor:

// Node.js — configure keepAliveTimeout maior que o idle timeout do ALB
server.keepAliveTimeout = 65000; // 65 segundos
server.headersTimeout = 66000;   // deve ser maior que keepAliveTimeout

O headersTimeout precisa ser maior que o keepAliveTimeout — caso contrário, o Node.js pode fechar a conexão antes de terminar de ler os headers de uma nova requisição que chegou exatamente no limite do timeout.

IAM Mínimo para Acesso aos Logs do ALB

Para que a equipe de operações consiga acessar os access logs sem permissões excessivas:

🔽 Clique para expandir — Política IAM mínima para logs do ALB
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DescribeALBAttributes",
      "Effect": "Allow",
      "Action": [
        "elasticloadbalancing:DescribeLoadBalancers",
        "elasticloadbalancing:DescribeLoadBalancerAttributes",
        "elasticloadbalancing:DescribeTargetGroups",
        "elasticloadbalancing:DescribeTargetGroupAttributes",
        "elasticloadbalancing:DescribeTargetHealth"
      ],
      "Resource": "*"
    },
    {
      "Sid": "ModifyALBAttributes",
      "Effect": "Allow",
      "Action": [
        "elasticloadbalancing:ModifyLoadBalancerAttributes",
        "elasticloadbalancing:ModifyTargetGroupAttributes"
      ],
      "Resource": [
        "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/meu-alb/*",
        "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/meu-tg/*"
      ]
    },
    {
      "Sid": "ReadALBLogsFromS3",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::meu-bucket-logs",
        "arn:aws:s3:::meu-bucket-logs/alb-logs/*"
      ]
    }
  ]
}

Note que as ações Describe* do Elastic Load Balancing exigem "Resource": "*" — não suportam restrição por ARN específico conforme documentado no Service Authorization Reference da AWS.

Métricas do CloudWatch para Monitorar 502s no ALB

Configure um alarme na métrica HTTPCode_ELB_502_Count no namespace AWS/ApplicationELB para detecção proativa:

aws cloudwatch put-metric-alarm \
  --alarm-name 'ALB-502-Errors' \
  --namespace AWS/ApplicationELB \
  --metric-name HTTPCode_ELB_502_Count \
  --dimensions Name=LoadBalancer,Value=app/meu-alb/1234567890abcdef \
  --statistic Sum \
  --period 60 \
  --evaluation-periods 2 \
  --threshold 5 \
  --comparison-operator GreaterThanOrEqualToThreshold \
  --alarm-actions arn:aws:sns:us-east-1:123456789012:meu-topico-alertas \
  --treat-missing-data notBreaching

Complemente com HTTPCode_Target_5XX_Count para distinguir erros originados no backend dos gerados pelo próprio ALB. Se HTTPCode_ELB_502_Count sobe mas HTTPCode_Target_5XX_Count permanece baixo, o problema está na camada de protocolo ou conexão — não na lógica da aplicação.

Conclusão e Próximos Passos com ALB 502

Um 502 no ALB com targets saudáveis quase sempre aponta para uma das três camadas: protocolo HTTP inválido, desalinhamento de timeout de conexão, ou ciclo de vida de target durante deploys. Os access logs do ALB são o ponto de partida obrigatório — especificamente os campos target-status-code e target-processing-time, que revelam se o ALB chegou a receber alguma resposta do backend.

  • Consulte a documentação oficial de access logs do ALB para o formato completo dos campos de log.
  • Revise os códigos de erro do ALB para entender as causas documentadas de cada código 5xx.
  • Se o problema for intermitente e difícil de reproduzir, considere habilitar o AWS X-Ray no ALB para rastreamento distribuído de requisições.

Glossário

TermoDefinição Operacional
Target GroupAgrupamento lógico de backends (instâncias EC2, IPs, funções Lambda) que recebem tráfego do ALB. Cada target group tem seu próprio health check e protocolo configurado.
Idle TimeoutTempo máximo que o ALB mantém uma conexão aberta sem atividade. Após esse período, a conexão é encerrada pelo ALB. Padrão: 60 segundos.
Keep-AliveMecanismo HTTP que permite reutilizar uma conexão TCP para múltiplas requisições, evitando o overhead de estabelecer nova conexão a cada request.
Deregistration DelayPeríodo que o ALB aguarda para drenar conexões ativas de um target antes de removê-lo definitivamente do target group.
HTTPCode_ELB_502_CountMétrica do CloudWatch que conta respostas 502 geradas pelo próprio ALB — distintas de 502s retornados pelos backends.

Comentários

Postagens mais visitadas deste blog

EC2 SSH Connection Timeout: Quais Regras do Security Group Verificar

EC2 Sem Acesso à Internet em VPC Customizada: Como Configurar Internet Gateway e Route Table

S3 Access Denied: Por que 'Bloquear Acesso Público' impede seu objeto mesmo após torná-lo público