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.

Pré-requisitos

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.

Sistema Ubuntu 24.04 LTS
PHP 8.3 (php-fpm)
Config do pool /etc/php/8.3/fpm/pool.d/www.conf
Socket Unix /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.

01

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

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

02

Com o pool sob carga, liste os workers ordenados por RSS:

ps -ylC php-fpm8.3 --sort:rss

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

03

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.

04

Veja a RAM total e o uso atual:

free -m

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

05

Aplique a fórmula. Com 2,5 GB pra PHP e RSS médio de 80 MB por worker:

2560 MB ÷ 80 MB = 32 workers

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

Swap é pior 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.

06

Edite o pool:

sudo nano /etc/php/8.3/fpm/pool.d/www.conf

Localize 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 = 500

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

07

Habilite o status page do pool — você vai precisar dele pra monitorar. Descomente ou adicione:

pm.status_path = /fpm-status

Salve e feche o arquivo.

08

Valide a sintaxe antes de aplicar:

sudo php-fpm8.3 -t

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

09

Recarregue o PHP-FPM sem matar requests em andamento:

sudo systemctl reload php8.3-fpm

Reload é 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.

10

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.

11

Teste a config e recarregue:

sudo nginx -t && sudo systemctl reload nginx

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

Não use pm = static sem capacity planning

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.

Tópicos:
Próximos passos Cloud Ryzen com NVMe e proteção DDoS sempre ativa.Coloque em produção numa VPS Hostini →
Esse tutorial foi útil?
Falar no WhatsApp