Rebootando 2× ao dia: o rate-limit que não funcionava porque o nginx via o IP errado (real_ip atrás de CDN)
Publicado em 24 de julho de 2026

O sintoma: reboots diários sob scraping
Um EC2 t4g.medium (2 vCPU) servindo um WordPress multisite atrás de uma CDN reiniciava duas vezes por dia. Sob carga, os 10 workers do PHP-FPM saturavam a ~100% de CPU, o load subia para 4,70 (numa máquina de 2 vCPU) e o tempo de resposta médio ia a 15,77s. Era scraping distribuído — muitos IPs, poucas requisições cada — e o rate-limit não barrava nada.
A causa: o nginx contava o IP da CDN
Havia uma zona de rate-limit de 12 req/min por $binary_remote_addr. O problema: atrás de uma CDN, o $remote_addr que o nginx enxerga é o IP do PoP da CDN, não o do cliente. Como todo o tráfego chega "da CDN", o limite agrega milhares de clientes (e atacantes) sob poucos IPs — e nunca dispara corretamente. A CDN tinha adicionado 2 PoPs novos cujos ranges não estavam no set_real_ip_from, então o real_ip nem era reconstruído para eles.
# nginx precisa confiar nos ranges da CDN p/ ler o IP real do X-Forwarded-For
set_real_ip_from 198.51.100.0/24; # PoP da CDN (exemplo)
set_real_ip_from 203.0.113.0/24; # PoP novo que faltava → real_ip não reconstruído
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# só então o rate-limit conta o cliente certo:
limit_req_zone $binary_remote_addr zone=perip:10m rate=12r/m;Rate-limit por IP atrás de CDN só funciona se o nginx reconstruir o IP real primeiro. Sem set_real_ip_from cobrindo TODOS os PoPs, você limita a CDN — não o atacante.
O erro que cometemos: bloquear a própria CDN
Na pressa, uma tentativa foi bloquear as faixas de onde vinha "o tráfego" — que eram os PoPs da CDN. Isso derruba o site inteiro para todo mundo. A lição: antes de bloquear um range, confirme se ele é do atacante ou da sua própria borda. Com o real_ip correto, os IPs de origem reais aparecem e o bloqueio mira o lugar certo.
O segundo ajuste: regex de paginação
O scraping batia em URLs de paginação profundas. A regra que detectava o padrão usava {3,} (3+ dígitos), deixando passar as páginas de 2 dígitos. Apertar para {2,} pegou a maior parte do abuso sem afetar navegação legítima:
# antes: só pegava /page/100+; depois: pega /page/10+
location ~* "/page/[0-9]{2,}" { limit_req zone=perip burst=5 nodelay; }O resultado
Tempo de resposta médio : 15,77s -> < 1s
Load (2 vCPU) : 4,70 -> 0,24
PHP-FPM CPU : ~100% -> normal
Reboots/dia : 2 -> 0Detalhe operacional: o journald não era persistente, então os logs dos crashes anteriores se perderam. Persistir o journal (Storage=persistent) foi parte do fix — sem log, cada reboot era um mistério novo.
Lições
1. set_real_ip_from precisa cobrir TODOS os PoPs da CDN. Um PoP novo fora da lista quebra o real_ip e cega o rate-limit.
2. Nunca bloqueie um range sem confirmar a origem. Bloquear os IPs da própria CDN derruba o site para todos.
3. journald persistente ou você perde a evidência. Sem Storage=persistent, cada reboot apaga o motivo do reboot anterior.
Conclusão
Rate-limit é tão bom quanto o IP que ele conta. Atrás de uma CDN, o nginx vê o PoP — e um limite por IP vira inútil até você reconstruir o IP real com set_real_ip_from para todos os ranges da borda. Com o IP certo, 12 req/min voltou a significar 12 req/min por cliente, e o servidor parou de reiniciar.