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

Aviso de afiliados v3.1
Aviso de afiliados v3.2 (somente celular)
Ambiente de teste Família RHELAlmaLinux 9.8conta rootVerificado em 12/10/2026

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'
58
grep '09-Oct-2026' /var/log/php-fpm/error.log | grep 'max_children' | cut -c2-21
09-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:44

Os 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.05
free -m

Os 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.x
grep '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 404

Em 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.

🧭 Simulador do caminho da requisiçãoPágina de administração wp-adminSimulador explicativo
🗂️ Requisições que lotaram os workers
Requisições wp-admin de um bot
Quando alguém sem login pede uma URL de administração, o WordPress responde com um 302 para a página de login. Esse único 302 é uma execução do PHP.
Scanner: arquivos tipo .env
Requisições a arquivos como .env são cortadas pela regra de bloqueio do nginx com um 444. 0,04 segundo, e o PHP nem é tocado.
Scanner: caminhos falsos comuns
Caminhos comuns fora das regras de bloqueio, como /userfiles ou /secrets.yml, fazem o WordPress gerar uma página 404. 266KB e 1,4–2,9 segundos, o mesmo custo de gerar um post normal.
Preload do cache
Salvar um post ou uma tradução esvazia o cache, e o preload do WP Rocket pede todas as URLs de novo. Até 60 por minuto, cada requisição usando um worker.
Visita a um post em cache
Se houver arquivo de cache, o nginx responde direto e o PHP nem é tocado.
Visita a um post sem cache
Se não houver arquivo de cache, o PHP gera a página e salva esse HTML como arquivo de cache.
Fluxo completo
A requisição desce de cima para baixo e para na camada em que a resposta termina. Só as requisições que descem até o WordPress usam um worker do PHP.
SolicitaçãoPara emRespostaWorkers
wp-admin do botWordPress302 (para a página de login)Usa 1
Scanner tipo .envRegra de bloqueio do nginx444 (0,04 s)Não usado
Scanner caminho falso comumWordPressPágina 404 (266KB · 1,4–2,9 s)Usa 1
Preload do cacheWordPress200 · salvo como arquivo de cacheUsa 1
Visita a um post em cacheArquivo de cache200 (o nginx responde direto)Não usado
Visita a um post sem cacheWordPress200 · salvo como arquivo de cacheUsa 1
Um simulador explicativo que roda só no seu navegador. As respostas e os tempos vêm de logs e medições de um servidor com Nginx + Apache em outubro de 2026.

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 -c

Se 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.

📊Antes e depois do limite de taxaPor dia · contagem de logs
AntesDepois
Respostas 302 de URLs de administração
302 que o WordPress enviou a requisições wp-admin sem login. O limite em URLs de administração sem login entrou no ar na noite de 9 de outubro.
02,4994,9987,4979,996
10/99,996Antes
10/1011Depois
10/111Depois
Requisições do IP do bot
Requisições diárias do único IP que enviou 22.391 em três horas.
05,59811,19616,79322,391
10/922,391Antes
10/100Nenhuma requisição
10/110Nenhuma requisição
Páginas 404 geradas pelo WordPress
Requisições do scanner a caminhos falsos comuns. O limite em requisições que chegam ao PHP entrou no ar na noite de 11 de outubro, então a avaliação fica para a verificação da próxima semana.
06531,3061,9592,612
10/92,563Antes
10/102,612Antes
10/111,630Antes · aplicado na noite do dia 11
Os números são contagens diárias do access.log do nginx de 9 a 11 de outubro de 2026.

🔢 FAQ & conteúdo recomendado

O dynamic cria e encerra workers conforme o volume de requisições e registra o aviso seems busy quando os workers ociosos chegam a 0. O static mantém sempre max_children workers, então não há atraso de criação nem aviso, mas a memória fica sempre ocupada. Este servidor ficou em dynamic com 26; se você tiver memória sobrando e o volume de requisições for constante o dia todo, o static é o certo.

Ligue quando certas requisições parecerem demorar nos horários dos avisos. Defina o caminho [slowlog] e o [request_slowlog_timeout] na configuração do pool, e o ponto de execução de cada requisição que passar desse tempo é registrado. Neste caso, a causa era o número de requisições, então bastou contar o access.log e o slowlog não foi usado.

O limite só vale para requisições sem login que passam pelo cache e chegam ao PHP, e só o excesso recebe 429 quando um IP passa de 30 por minuto ou 60 de uma vez. Na contagem de 11 de outubro, o único IP acima dessa linha era um scanner, e o estado de excesso some em um minuto, então a próxima requisição recebe resposta normal.

Não reduzi. O preload se concentrou nos horários em que posts e traduções foram salvos e nunca causou erros para visitantes. O critério não é o número de avisos, e sim se os visitantes receberam erros 502 ou 504; se esse número for 0, o preload fica como está.

ℹ️ 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.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Demonstre sua humanidade: 7   +   1   =