Aviso de max_children do php-fpm: descubra a causa nos logs antes de aumentar os workers

O aviso max_children do php-fpm é sinal de que todos os processos worker do PHP estão ocupados com requisições, e por isso as novas foram para a fila. Num servidor WordPress com Nginx, Apache e php-fpm em que esse aviso vinha se acumulando havia semanas, usei os logs para apontar as requisições que lotavam os workers e resolvi com um limite de taxa no nginx em vez de aumentar a configuração.
Na verificação semanal, o aviso foi registrado de 27 a 187 vezes por dia, mas nos mesmos quatro dias os visitantes receberam zero erros 502 ou 504. Quem lotava os workers não eram visitantes, e sim um bot martelando URLs de administração e um scanner pedindo caminhos que não existem.
1️⃣ php-fpm max_children & seems busy
Quando o aviso chega a 66 por dia, é fácil ler isso como 66 vezes em que faltaram workers. O log do php-fpm mistura dois avisos diferentes, e a maioria são avisos prévios registrados antes de o limite ser atingido.
[09-Oct-2026 00:16:18] WARNING: [pool www] seems busy (you may need to increase pm.start_servers, or pm.min/max_spare_servers), spawning 8 children, there are 0 idle, and 15 total children
[09-Oct-2026 13:02:05] WARNING: [pool www] server reached pm.max_children setting (26), consider raising it[seems busy] significa que há 0 workers ociosos e um novo processo está sendo criado; com o gerenciamento de processos em dynamic, até um breve aumento de requisições o registra. [server reached pm.max_children] é o momento em que o limite de 26 foi atingido e não dava para criar mais, e a partir daí as novas requisições esperam.
É a diferença entre chamar mais um garçom e deixar os clientes esperando do lado de fora porque todas as mesas estão ocupadas.
Contando um dia por tipo, deu 58 seems busy e 6 max_children. A rotina de verificação somou os dois e relatou 66, mas o limite foi atingido de fato seis vezes.
grep '09-Oct-2026' /var/log/php-fpm/error.log | grep 'pool www' | grep -c 'seems busy'58grep '09-Oct-2026' /var/log/php-fpm/error.log | grep 'max_children' | cut -c2-2109-Oct-2026 13:02:05
09-Oct-2026 13:52:10
09-Oct-2026 18:09:20
09-Oct-2026 18:42:17
09-Oct-2026 18:46:07
09-Oct-2026 19:12:44Os seis horários que o segundo comando mostra viram a referência para rastrear a causa; a próxima etapa usa só esses horários.
2️⃣ Por que aumentar o max_children não é a resposta
Aumentar o limite de 26 pode reduzir o número de avisos, mas não muda quais requisições lotaram os workers.
O cálculo comum divide a memória disponível pela memória média de um worker. No servidor medido, 26 workers usavam em média 131MB e no máximo 214MB, e calculando com a memória disponível restante, no papel havia margem para criar mais workers.
ps -o rss= -u www -C php-fpm | awk '{s+=$1; n++; if($1>m) m=$1} END{print n, s/n/1024, m/1024, s/1024}'26 130.579 214.27 3395.05free -mOs quatro valores da primeira saída são número de workers, memória média (MB), memória máxima (MB) e total (MB); a coluna available da saída do free é a memória disponível.
Quando há mais workers do que núcleos de CPU, mais workers não aumentam as requisições atendidas ao mesmo tempo; só alongam o tempo de cada uma. Enquanto os visitantes recebem zero erros 502 ou 504, aumentar o limite não traz ganho e só dá mais processos aos bots e scanners que ocupavam os workers.
Antes de aumentar o limite, confira se as requisições que ocupavam workers nos horários dos avisos eram de visitantes ou não; a próxima seção mostra como verificar isso no access.log.
3️⃣ Rastrear a causa no access.log: horário, IP, caminho, código de status
Escolha um horário de aviso e a resposta está no access.log daquele 1–2 minuto. A ordem é: extrair as linhas pelo horário, contar por IP e depois conferir os caminhos e códigos de status dos IPs do topo.
grep '09/Oct/2026:18:09' /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head 115 140.245.x.x
61 187.15.x.x
17 3.36.x.x
8 91.239.x.x
4 79.127.x.x
3 109.248.x.x
3 109.248.x.x
3 109.248.x.x
2 79.127.x.x
2 40.77.x.xgrep '09/Oct/2026:18:09' /var/log/nginx/access.log | awk '{print $7, $9}' | sort | uniq -c | sort -rn | head 55 / 200
13 /wp-login.php 404
13 /wp-admin/plugins.php 302
13 /wp-admin/index.php 302
13 /wp-admin/edit.php 302
12 /wp-admin/profile.php 302
4 / 499
3 /xmlrpc.php 302
2 /xmlrpc.php 499
2 /wp-content/themes/kinsta/images/duos/small/wheelchair.svg 404Em 9 de outubro, às 18h09, foram registradas 286 requisições em um minuto; um IP respondia por 115 delas, os caminhos eram URLs de administração como /wp-admin/profile.php e todos os códigos de status eram 302.
O código de status diz se o PHP foi alcançado. Um 200 de HTML em cache entregue direto pelo nginx, e 444 ou 400, nunca tocam o PHP, enquanto 302, 404 e um 200 sem cache usam um worker cada. Quando alguém sem login pede uma URL de administração, o WordPress responde com um 302 para a página de login, e esse único 302 é uma execução do PHP.
Se um servidor hospeda dois ou mais sites, o access.log global registra linhas de todos. No começo achei que esse bot atacava o primeiro site, mas comparar com o log próprio de cada site mostrou que 22.390 das 22.391 requisições eram para o segundo site.
4️⃣ Três tipos de requisição que lotaram os workers: wp-admin de um bot, 404 de um scanner, preload do cache
Aplicando a mesma ordem a todos os horários de aviso, apareceram três tipos de requisição lotando os workers.
O primeiro era um bot pedindo URLs de administração sem parar. Um IP enviou 22.391 requisições em três horas; 9.944 delas foram para quatro caminhos, entre eles /wp-admin/profile.php, e como cada 302 sem login é uma execução do PHP, ele usou um worker 9.944 vezes.
O segundo era um scanner de vulnerabilidades pedindo caminhos que não existem. Mesmo com Googlebot no User-Agent, não é o Googlebot: o IP que enviou 14.672 requisições por dia era um servidor virtual do Google Cloud cujo DNS reverso termina em googleusercontent.com, enquanto o Googlebot termina em googlebot.com. O mesmo IP se revezava entre bingbot, Applebot, GPTBot e ClaudeBot.
Requisições a arquivos como .env são cortadas pelo nginx com um 444 em 0,04 segundo, mas caminhos comuns fora das regras de bloqueio, como /userfiles ou /secrets.yml, fazem o WordPress gerar uma página 404. Essa página 404 custa 266KB e 1,4–2,9 segundos, o mesmo que gerar um post normal, e um IP a pediu 4.069 vezes por dia.
O terceiro era o próprio servidor. Salvar um post ou uma tradução esvazia o cache, e o preload do WP Rocket pede todas as URLs de novo, o que deu 8.489 requisições por dia e até 60 por minuto. As verificações de status de indexação do Rank Math também usaram um worker 754 vezes por dia via Action Scheduler. Os dois se concentraram no horário de trabalho e não causaram erros para visitantes, então deixei-os fora da correção.
No simulador abaixo, você vê em que camada cada requisição termina e se usa um worker do PHP.
| Solicitação | Para em | Resposta | Workers |
|---|---|---|---|
| wp-admin do bot | WordPress | 302 (para a página de login) | Usa 1 |
| Scanner tipo .env | Regra de bloqueio do nginx | 444 (0,04 s) | Não usado |
| Scanner caminho falso comum | WordPress | Página 404 (266KB · 1,4–2,9 s) | Usa 1 |
| Preload do cache | WordPress | 200 · salvo como arquivo de cache | Usa 1 |
| Visita a um post em cache | Arquivo de cache | 200 (o nginx responde direto) | Não usado |
| Visita a um post sem cache | WordPress | 200 · salvo como arquivo de cache | Usa 1 |
5️⃣ Limite de taxa no nginx: limitar requisições sem login e restaurar o IP real
Escolhi limitar a taxa em vez de bloquear. Bloqueie um caminho e o scanner troca de caminho; bloqueie um IP e no dia seguinte aparece outro. Só as requisições sem login que chegam ao PHP são limitadas, a 30 por minuto e 60 de uma vez por IP, e o excesso recebe 429.
Para um servidor atrás de um CDN, só essa configuração não basta. O IP que o nginx vê é o endereço do edge do CDN, então todos os visitantes que passam por aquele edge viram um só IP; primeiro é preciso registrar a lista de IPs dos edges com set_real_ip_from para restaurar o IP real do X-Forwarded-For. Requisições com cookie de login ficam isentas, mas nada é isento pelo User-Agent.
# contexto http
real_ip_header X-Forwarded-For;
real_ip_recursive on;
set_real_ip_from 203.0.113.0/24; # lista de edges do CDN
map $http_cookie $php_anon_key {
default $binary_remote_addr;
~*wordpress_logged_in '';
}
limit_req_zone $php_anon_key zone=php_anon:10m rate=30r/m;
# location que passa para o PHP
location @apache {
limit_req zone=php_anon burst=60 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:8080;
}Páginas em cache são respondidas dentro do nginx pelo try_files e nunca chegam a esta location, e o admin-ajax.php é uma location separada, por isso fica fora do limite.
Teste com requisições simultâneas. Requisições sequenciais nunca passam de 30 por minuto porque um 404 leva 1,5 segundo, então nunca batem no limite; enviar 100 requisições, 20 de cada vez, dá 67 respostas 404 e 33 respostas 429, o que confirma que o limite funciona.
seq 1 100 | xargs -P 20 -I{} curl -s -o /dev/null -w '%{http_code}\n' https://example.com/zz-{} | sort | uniq -cSe a saída contar 429, o limite está funcionando. Se você isentou o IP do próprio servidor, tire a isenção por um momento durante o teste ou rode de outra rede.
6️⃣ Antes e depois
O limite em URLs de administração sem login entrou no ar na noite de 9 de outubro, e o limite em requisições que chegam ao PHP na noite do dia 11. Os 302 de URLs de administração caíram de 9.996 no dia 9 para 11 no dia 10 e 1 no dia 11, e o IP que enviou 22.391 requisições em três horas não enviou mais nenhuma desde o dia seguinte.
Para o scanner, o limite acabou de ser aplicado, então ainda não há números de um dia inteiro; a avaliação virá de quanto cair, na verificação da próxima semana, o número de páginas 404 geradas pelo WordPress (2.563 no dia 9, 2.612 no dia 10, 1.630 no dia 11). Os erros 502 e 504 de visitantes foram 0 antes e depois.
Esses números foram contados direto dos logs de um servidor WordPress com Nginx, Apache e php-fpm, e a avaliação não se baseou no número de avisos, e sim nos erros de visitantes e nas requisições que chegam ao PHP.
🔢 FAQ & conteúdo recomendado
Cache
Cache do site & cache interno do servidor
Explica, camada por camada, como as requisições em cache evitam o PHP.
Segurança
Vulnerabilidades do wp-config.php
Como proteger com permissões e regras de servidor o arquivo de configuração que os scanners miram.
Linux
15 categorias de comandos Linux
Uma tabela que divide os comandos em 15 categorias, incluindo o grep e o awk usados para contar os logs.
ℹ️ Aviso de afiliados
O conteúdo deste site contém links de afiliados. Quando um visitante compra um produto ou serviço por meio de um desses links, o site recebe uma comissão do vendedor. O valor que o comprador paga (em promoções, ele diminui ↓) não aumenta por causa disso. Preços, descontos e informações de estoque valem para o momento em que o texto foi escrito e podem ter mudado, por isso confirme com o vendedor antes de comprar. Os produtos são escolhidos e avaliados por critérios próprios, e o recebimento de comissão não influencia a ordem de apresentação nem o conteúdo das avaliações.