Buzeli
buzeliSoluções Digitais
Incidentes

363 mil falsos 429 em um dia: o bug de rate limit que o Gutenberg revelou

Publicado em 7 de abril de 2026

363 mil erros 429 falsos em um dia — bug de rate limit revelado pelo Gutenberg no nginx com ALB

O problema: 363 mil erros 429 em um único dia

Um WordPress de alto tráfego começou a retornar erro 429 (Too Many Requests) para usuários legítimos de forma intermitente. O rate limit havia sido configurado com limit_req_zone usando $binary_remote_addr — o que parecia correto. Mas os bloqueios aconteciam em massa, sem padrão de abuso real. Em 20 dias de investigação, foram 905.311 erros acumulados. O pico: 363.000 em um único dia.

O editor Gutenberg foi o gatilho visível. Ao abrir um post para edição, ele dispara mais de 50 requisições paralelas de assets (JS, CSS, fontes). Com burst=10 configurado, qualquer editor logado estourava o rate limit instantaneamente — mas o problema não era o burst.

A causa raiz: $binary_remote_addr e o ALB multi-AZ

Em produção atrás de um Application Load Balancer AWS, o nginx não enxerga o IP real do cliente — ele enxerga o IP do nó do ALB que encaminhou a requisição. O ALB em configuração multi-AZ opera com um nó em cada subnet de cada Availability Zone. Todos os clientes chegavam através de um número reduzido de IPs de nós do ALB.

O efeito prático: com $binary_remote_addr apontando para o IP do ALB, o rate limit contabilizava as requisições de todos os usuários como se fossem de um único cliente. Com qualquer tráfego moderado, o bucket compartilhado transbordava e retornava 429 para todos.

A diretiva real_ip_module do nginx resolve isso — ela lê o IP real do cliente a partir do header X-Forwarded-For, mas exige que os IPs confiáveis (os nós do ALB) sejam explicitamente declarados com set_real_ip_from. O erro era ter declarado apenas a subnet da primeira AZ, ignorando as subnets das demais AZs.

Configuração antes da correção

Copiar
# nginx.conf — configuração incompleta (só 1 AZ declarada)
limit_req_zone $binary_remote_addr zone=wp_limit:10m rate=10r/s;

# Apenas 1 subnet declarada
set_real_ip_from 10.0.0.0/22;   # ALB us-east-1a
real_ip_header X-Forwarded-For;

Com apenas uma subnet declarada, requisições roteadas por nós do ALB nas outras AZs (10.0.4.0/22 e 10.0.8.0/22) não passavam pelo real_ip_recursive — o nginx mantinha o IP do nó do ALB como endereço de origem.

A correção: 3 linhas no nginx.conf

Copiar
# nginx.conf — configuração correta (todas as AZs declaradas)
limit_req_zone $binary_remote_addr zone=wp_limit:10m rate=10r/s;

set_real_ip_from 10.0.0.0/22;   # ALB us-east-1a
set_real_ip_from 10.0.4.0/22;   # ALB us-east-1b ← adicionada
set_real_ip_from 10.0.8.0/22;   # ALB us-east-1c ← adicionada
real_ip_header X-Forwarded-For;
real_ip_recursive on;

real_ip_recursive on é essencial quando há múltiplos proxies encadeados. Com ele, o nginx percorre toda a cadeia X-Forwarded-For de direita para esquerda e descarta os IPs que estão na lista de confiáveis, chegando ao IP real do cliente.

Por que o Gutenberg foi o gatilho visível

O editor de blocos do WordPress carrega mais de 50 assets JavaScript e CSS de forma paralela ao abrir qualquer post para edição. Com um burst=10 configurado, essa rajada inicial já ultrapassava o limite — mas com o IP do ALB no lugar do IP real, o problema era exponencialmente maior: todos os editores no cluster WordPress compartilhavam o mesmo bucket de rate limit.

O sintoma era consistente: 429 imediato ao abrir o editor, recuperação esporádica ao recarregar, bloqueios retornando. Usuários em sessões de edição pesada reportavam erros intermitentes sem padrão claro — exatamente o comportamento esperado de um bucket compartilhado sendo esgotado coletivamente.

Durante 20 dias, a hipótese de ataque DDoS foi investigada e descartada. Os logs mostravam IPs 'atacantes' que eram, na verdade, os nós do próprio ALB. O problema nunca foi volume de tráfego — foi confiança de proxy mal configurada.

Diagnóstico: como identificar o problema

Para verificar qual IP o nginx está recebendo como endereço de origem:

Copiar
# Adicionar log temporário para inspecionar o IP de origem
log_format debug_ip '$remote_addr - $http_x_forwarded_for - $request';
access_log /var/log/nginx/debug_ip.log debug_ip;

Se $remote_addr mostrar IPs de faixa privada (10.x.x.x, 172.x.x.x) enquanto $http_x_forwarded_for contiver IPs reais de clientes, o real_ip_module não está resolvendo corretamente. O motivo: subnets do ALB não declaradas no set_real_ip_from.

Para listar os IPs de nós do ALB ativos:

Copiar
# Via AWS CLI — listar IPs dos nós do ALB por AZ
aws elbv2 describe-load-balancers --names nome-do-alb \
  --query 'LoadBalancers[0].AvailabilityZones[*].{AZ:ZoneName,SubnetId:SubnetId}'

# Ou via dig, resolvendo o DNS do ALB
dig +short nome-do-alb.us-east-1.elb.amazonaws.com

Atenção: use as subnets (CIDRs) no set_real_ip_from, não os IPs individuais dos nós. Os IPs dos nós do ALB mudam a cada redeploy, substituição de nó ou scaling event. Os CIDRs das subnets são estáveis.

Lições aprendidas

1. Sempre use $http_x_forwarded_for no diagnóstico de rate limit atrás de proxy.

2. set_real_ip_from deve cobrir todas as subnets de todas as AZs do ALB — não apenas a AZ primária.

3. Inclua real_ip_recursive on quando há múltiplos proxies (ALB → nginx, por exemplo).

4. Antes de investigar DDoS ou abuso, verifique se os IPs nos logs são realmente de clientes externos ou de proxies internos.

5. O Gutenberg como gatilho é previsível: carregamento paralelo de 50+ assets ultrapassa qualquer burst conservador. Exceções de rate limit para /wp-admin e /wp-json devem ser consideradas.

Resultado

Após adicionar as 3 linhas (as duas subnets ausentes e real_ip_recursive on), os erros 429 caíram para zero imediatamente. Em 20 dias de investigação e 905.311 erros acumulados, a correção levou menos de 5 minutos para ser aplicada e não exigiu restart do nginx — apenas reload.

A correção mais rápida costuma ser a última testada. Neste caso, o foco em análise de tráfego e ajustes de burst adiou a investigação da configuração de proxy. A premissa 'rate limit configurado corretamente' estava errada desde o início.