Otimizar PHP-FPM Pool em VPS Linux: pm.max_children e Fim do 502
Resolva 502 Bad Gateway calculando pm.max_children, pm.start_servers e pm.max_requests no pool PHP-FPM da sua VPS Linux com Nginx.
PHP-FPM com pool mal dimensionado é a causa número um de 502 Bad Gateway em VPS rodando Nginx + PHP. O sintoma aparece sob carga moderada — 50, 100 requests simultâneos — e desaparece quando o tráfego cai, levando o diagnóstico pra direções erradas (Nginx, banco, rede). Na realidade, o pool está exausto: todos os workers PHP-FPM ocupados, o Nginx tenta enviar o request via FastCGI, recebe negação ou timeout, e devolve 502 pro cliente.
Este tutorial é pra você developer ou sysadmin com VPS Linux rodando
Nginx + PHP-FPM em qualquer aplicação PHP (Laravel, WordPress, Symfony,
Magento) que está vendo 502 esporádicos em produção ou simplesmente quer
dimensionar o pool com base em medição, não em valores copiados de blog.
Vamos cobrir como medir a memória real dos workers, calcular
pm.max_children pela fórmula correta, ajustar os parâmetros auxiliares
(pm.start_servers, pm.min_spare_servers, pm.max_spare_servers,
pm.max_requests) e validar a configuração com o Nginx servindo carga real.
Tempo estimado de execução: 25 minutos, incluindo um teste de carga controlado pra validar antes/depois.
Pré-requisitos
Você precisa de VPS Linux com privilégios root ou sudo, PHP-FPM instalado e funcional, e Nginx já roteando requests pro socket Unix do PHP-FPM. Os exemplos usam Ubuntu 24.04 LTS com PHP 8.3, mas os princípios se aplicam a qualquer versão do PHP-FPM (7.4 em diante) e a Debian, AlmaLinux ou Rocky Linux com ajustes mínimos nos caminhos de arquivo.
Acesso SSH ativo, sudo, e a aplicação PHP rodando em produção ou staging com tráfego representativo. Você vai precisar coletar métricas com o pool sob carga real — medir em idle dá números errados.
Ubuntu 24.04 LTS 8.3 (php-fpm) /etc/php/8.3/fpm/pool.d/www.conf /run/php/php8.3-fpm.sock Medir a memória real dos workers
Antes de tocar em qualquer parâmetro, você precisa saber quanto cada worker PHP-FPM consome de memória residente (RSS) com sua aplicação específica. Esse número varia drasticamente entre stacks — um worker Laravel com Eloquent pesado pode consumir 80–120 MB, um WordPress simples fica em 40–60 MB, e uma API Symfony otimizada pode rodar com 30 MB. Chutar 60 MB porque “é o padrão” é a forma mais comum de subdimensionar o pool.
Gere carga representativa na aplicação. Em staging, use uma ferramenta
como ab (ApacheBench) ou wrk contra os endpoints mais pesados:
ab -n 1000 -c 20 https://seu-dominio.com/endpoint-pesadoEm produção, pode ser o tráfego natural durante horário de pico. O objetivo é forçar os workers a processarem requests reais e ficarem em estado pós-execução com a aplicação carregada em memória.
Com o pool sob carga, liste os workers ordenados por RSS:
ps -ylC php-fpm8.3 --sort:rssA saída tem uma coluna RSS em KB. Ignore o processo master (geralmente o primeiro, menor) e foque nos workers (estado S após terem processado requests). Esses são os que importam pra o cálculo.
Calcule a média de RSS dos workers ativos:
ps --no-headers -o rss -C php-fpm8.3 | awk '{sum+=$1; count++} END {print "Media:", sum/count/1024, "MB"}'Anote esse valor. Em uma aplicação Laravel típica, espere algo entre 70
e 110 MB. Esse é o número que vai pra fórmula de pm.max_children.
Calcular pm.max_children
A fórmula é simples e existe pra ser respeitada: pm.max_children = (RAM disponível pra PHP) ÷ (RSS médio por worker). O ponto não-óbvio é o
“disponível pra PHP” — não é a RAM total da VPS. Você precisa subtrair o
que MySQL, Redis, Nginx, kernel e buffers consomem em pico.
Veja a RAM total e o uso atual:
free -mEm uma VPS de 4 GB rodando MySQL (~800 MB), Redis (~150 MB), Nginx (~50 MB) e o sistema base (~400 MB), sobram aproximadamente 2,5 GB pra PHP. Em uma VPS de 2 GB com a mesma stack, sobram cerca de 600 MB — muito menos do que blogs costumam assumir.
Aplique a fórmula. Com 2,5 GB pra PHP e RSS médio de 80 MB por worker:
2560 MB ÷ 80 MB = 32 workersReserve uma margem de segurança de 15–20%. Use 26 ou 27 como
pm.max_children. Esse buffer evita que picos de RSS (workers
processando endpoints atípicos) empurrem o sistema pra swap, que mata
performance muito mais rápido que 502.
Se você estourar a RAM e o sistema cair no swap, latência sobe pra segundos e a VPS inteira fica não-responsiva. 502 ocasional é recuperável; thrashing de swap leva a outage completo. Sempre subdimensione antes de superdimensionar.
Ajustar o pool no www.conf
Com pm.max_children calculado, abra /etc/php/8.3/fpm/pool.d/www.conf
e ajuste os parâmetros relacionados. Eles não são independentes — valores
mal alinhados causam tanto churn que o ganho do max_children correto
desaparece em overhead de spawn.
Edite o pool:
sudo nano /etc/php/8.3/fpm/pool.d/www.confLocalize e ajuste estas linhas. Os valores abaixo assumem o exemplo de 26 workers calculado acima — adapte ao seu cálculo:
pm = dynamic
pm.max_children = 26
pm.start_servers = 7
pm.min_spare_servers = 4
pm.max_spare_servers = 10
pm.max_requests = 500A regra prática: start_servers ≈ 25% de max_children,
min_spare_servers ≈ 15%, max_spare_servers ≈ 40%. Isso mantém
workers ociosos suficientes pra absorver picos sem desperdiçar RAM em
idle.
Habilite o status page do pool — você vai precisar dele pra monitorar. Descomente ou adicione:
pm.status_path = /fpm-statusSalve e feche o arquivo.
Valide a sintaxe antes de aplicar:
sudo php-fpm8.3 -tA saída deve ser configuration file /etc/php/8.3/fpm/php-fpm.conf test is successful. Se der erro, o pool não vai subir e o site cai — não
prossiga com erro pendente.
Recarregue o PHP-FPM sem matar requests em andamento:
sudo systemctl reload php8.3-fpmReload é graceful: workers atuais terminam o que estão processando, o master spawna novos com a config nova, e a transição é transparente pro usuário. Restart só se reload falhar.
Expor o status no Nginx pra monitorar
O pm.status_path só funciona se o Nginx rotear o request pro PHP-FPM.
Sem isso você dimensiona às cegas e descobre o problema só quando o 502
volta a aparecer.
Edite o vhost do Nginx e adicione um location protegido:
location ~ ^/(fpm-status|fpm-ping)$ {
allow 127.0.0.1;
allow 10.0.0.0/8;
deny all;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $fastcgi_script_name;
}A restrição por IP é essencial — status público expõe carga e número de workers pra qualquer um na internet, o que é informação útil pra atacante planejando DoS.
Teste a config e recarregue:
sudo nginx -t && sudo systemctl reload nginxAcesse http://127.0.0.1/fpm-status via SSH (curl localhost) e
confirme que a página retorna métricas: accepted conn, listen queue,
active processes, max active processes.
Verificação
Gere carga novamente e observe duas métricas críticas no status. A
primeira é listen queue: se for maior que zero de forma consistente,
o pool está saturando e requests estão enfileirando — você precisa
aumentar max_children ou investigar lentidão na aplicação. A segunda é
max children reached: se esse contador subir, você bateu o teto pelo
menos uma vez desde o último reload.
curl -s http://127.0.0.1/fpm-status
Saída saudável sob carga:
pool: www
process manager: dynamic
accepted conn: 4823
listen queue: 0
max listen queue: 2
idle processes: 6
active processes: 11
total processes: 17
max active processes: 19
max children reached: 0
Se max children reached ficou em 0 durante o teste de carga, o
dimensionamento está correto. Se subiu, rode o ps novamente sob
carga, recalcule a RSS média (workers podem estar consumindo mais do que
você mediu inicialmente) e ajuste.
Resolução de problemas
502 continua aparecendo após o ajuste
Verifique /var/log/php8.3-fpm.log buscando a string server reached max_children setting. Se aparecer, seu cálculo subestimou a memória ou
a carga real é maior que o teste. Aumente max_children em 20% e refaça
o ciclo de medição. Se a mensagem não aparece mas o 502 persiste, o
problema não é pool — investigue fastcgi_read_timeout no Nginx
(padrão 60s pode ser baixo pra endpoints pesados) e queries lentas no
banco.
Workers consumindo muito mais RAM que o esperado
Memory leaks em aplicação PHP acumulam ao longo de centenas de requests.
Reduza pm.max_requests de 500 pra 200 — isso recicla workers mais
agressivamente e mata o leak antes dele crescer. Se o leak é muito
rápido (worker dobra de tamanho em 50 requests), o problema é código —
profile com xdebug.profiler_enable em staging pra encontrar a causa.
pm = static aloca todos os workers no boot do PHP-FPM. Se você
configurar pm.max_children = 100 em uma VPS de 2 GB e usar static,
o boot vai consumir toda a RAM imediatamente e o sistema entra em
thrashing antes do primeiro request chegar.
Próximos passos
Com o pool dimensionado, alguns próximos passos lógicos pra fortalecer a
stack: configurar OPcache com opcache.memory_consumption adequado pra
evitar recompilação a cada hit, ajustar fastcgi_read_timeout no Nginx
pra endpoints conhecidos como lentos, exportar métricas do pool pra um
sistema de observabilidade (Grafana, Datadog), e implementar health
checks no Nginx via fastcgi_next_upstream pra failover gracioso.
Se você está colocando isso em produção e quer começar com uma base
estável, uma VPS Hostini já vem com PHP-FPM 8.3 pré-instalado,
Nginx tuned, e SSD NVMe — o que reduz drasticamente o tempo de I/O por
request e impacta diretamente o pm.max_children que sua aplicação
precisa.
Perguntas frequentes
Qual é o valor ideal de pm.max_children?
Não existe valor ideal universal — depende da RSS média de cada worker e da RAM disponível. A fórmula é (RAM livre pra PHP) ÷ (RSS médio por processo). Em uma VPS de 4 GB com 60 MB por worker e 2 GB reservados pra PHP, são 33 workers. Meça antes de chutar.
Por que estou recebendo 502 Bad Gateway só sob carga?
Quase sempre é pool exausto: pm.max_children muito baixo ou workers travados em I/O. O Nginx envia request, PHP-FPM rejeita ou demora além de fastcgi_read_timeout, e o Nginx devolve 502. Verifique php-fpm.log buscando 'server reached max_children setting'.
Devo usar pm = dynamic, static ou ondemand?
dynamic é o padrão e funciona em 95% dos casos. static só faz sentido em VPS dedicado a uma única aplicação com tráfego previsível — elimina churn de spawn. ondemand economiza RAM em ociosidade mas adiciona latência no primeiro request — bom pra sites de baixo tráfego, ruim pra APIs.
pm.max_requests afeta performance?
Sim. Define quantos requests cada worker processa antes de ser reciclado. Valor baixo (100) mata leaks de memória rápido mas aumenta CPU em spawn. Valor alto (1000) é eficiente mas deixa leaks crescerem. 500 é um meio-termo seguro pra maioria das stacks (Laravel, WordPress, Symfony).
Como saber a RSS real dos meus workers PHP-FPM?
Rode `ps -ylC php-fpm8.3 --sort:rss` com o pool sob carga real (não em idle). A coluna RSS mostra memória residente em KB. Tire a média dos workers em estado S (sleeping após request) — esse é o consumo estável que importa pro cálculo de pm.max_children.
Preciso reiniciar PHP-FPM ou reload basta?
`systemctl reload php8.3-fpm` é suficiente pra mudanças em pm.* — releitura graceful sem matar requests em andamento. Use restart apenas se mudar opções globais (error_log, daemonize) ou se o pool estiver travado. Reload é seguro em produção.