← Voltar para o Blog

PHP-FPM: pool, workers e o 502 que volta sempre

O site cai no horário de pico, volta sozinho e ninguém acha erro no log do PHP. O problema quase nunca é o código — é a conta entre workers, memória e o que o nginx espera do outro lado.

Equipe EasyOps Cloud · · 7 min de leitura

Todo dia às onze da manhã o site devolve 502 Bad Gateway por alguns minutos. Depois volta sozinho. O log do PHP não registra erro, o log da aplicação termina no meio, e o top mostra a máquina com memória sobrando.

É o retrato clássico de PHP-FPM mal dimensionado. E a razão de ser difícil de diagnosticar é que o erro aparece no nginx, enquanto a causa está no processo que ele tenta acessar — dois logs diferentes, duas configurações que precisam concordar, e nenhuma delas reclama sozinha.

O que o 502 significa aqui

O nginx não executa PHP. Ele repassa a requisição ao PHP-FPM por um socket e espera. O 502 significa que essa conversa falhou — não que o PHP tenha dado erro.

Três causas produzem exatamente o mesmo código:

  • Não havia worker livre. Todos ocupados, a fila estourou, a conexão foi recusada.
  • O worker morreu no meio. Falta de memória, ou o próprio FPM o reciclou.
  • O socket está errado. Caminho, permissão ou dono incorretos.

O log do nginx distingue as três em uma linha:

sudo tail -50 /var/log/nginx/error.log

connect() to unix:/run/php/php8.3-fpm.sock failed (11: Resource temporarily unavailable) é fila cheia. (2: No such file or directory) é caminho errado. (13: Permission denied) é permissão. E recv() failed (104: Connection reset by peer) é worker que morreu.

Quantos workers cabem de fato

Aqui está a decisão central, e ela é aritmética simples que quase ninguém faz.

Cada worker do PHP-FPM é um processo com memória própria. O número máximo de workers multiplicado pelo consumo de cada um não pode ultrapassar a RAM disponível — senão o kernel começa a matar processos, e o site cai de um jeito bem pior.

Meça o consumo real por worker, em vez de estimar:

ps -o rss,cmd -C php-fpm8.3 --sort=-rss | head
ps --no-headers -o rss -C php-fpm8.3 | awk '{s+=$1; n++} END {printf "media: %.0f MB\n", s/n/1024}'

Uma aplicação enxuta fica em torno de 40 MB por worker; um sistema grande com muito framework passa de 150 MB. Com o número em mãos:

max_children = (RAM disponível para PHP) / (média por worker)

Em uma VPS de 4 GB rodando também o banco e o nginx, sobram talvez 2 GB para o PHP. Com 80 MB por worker, isso dá 25 — não 200, que é o valor que aparece em tutorial copiado.

Errar para cima é pior que errar para baixo. Com max_children alto demais, o pico consome toda a memória e a máquina inteira degrada; com valor conservador, as requisições excedentes esperam na fila e o site fica lento, mas de pé.

Escolher o gerenciador de processos

; /etc/php/8.3/fpm/pool.d/meusite.conf
[meusite]
user = meuapp
group = meuapp
listen = /run/php/meusite.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = dynamic
pm.max_children = 25
pm.start_servers = 6
pm.min_spare_servers = 4
pm.max_spare_servers = 10
pm.max_requests = 500

As três estratégias e onde cada uma serve:

pmComportamentoQuando usar
staticNúmero fixo de workers, sempre no arTráfego alto e constante
dynamicCresce e encolhe entre limitesPadrão para a maioria
ondemandCria sob demanda, encerra ociosoMuitos sites pequenos na mesma máquina

O pm.max_requests = 500 recicla o worker após 500 requisições. É a defesa barata contra vazamento de memória em extensão — o worker é recriado antes de crescer demais. Sem ele, um vazamento pequeno vira um problema grande depois de dias.

Se você hospeda vários sites na mesma máquina, use um pool por site, com usuário próprio cada um. Isso isola consumo e permissão: um site que estoura os workers não derruba os vizinhos, e um site comprometido não lê os arquivos dos outros.

Timeouts que precisam concordar

Um dos erros mais frequentes é configurar timeout em um lado só. Os três valores precisam estar alinhados, do maior para o menor:

; php.ini
max_execution_time = 60
; pool
request_terminate_timeout = 70
location ~ \.php$ {
    fastcgi_pass unix:/run/php/meusite.sock;
    fastcgi_read_timeout 75s;
}

A ordem importa. O PHP deve desistir primeiro, para que o erro seja registrado como erro de PHP — com stack trace e arquivo — em vez de virar um 504 genérico no nginx. Se o nginx desiste antes, você perde a informação de qual código travou.

O request_terminate_timeout é a rede de segurança: ele mata o worker quando o max_execution_time não dá conta, o que acontece em espera por rede ou por banco — esse tempo não conta para o PHP.

O slowlog resolve o "por que travou"

Quando os workers estão todos ocupados, a pergunta é o que eles estão fazendo. O slowlog responde com stack trace, sem instrumentar nada:

slowlog = /var/log/php-fpm/meusite-slow.log
request_slowlog_timeout = 5s
sudo tail -f /var/log/php-fpm/meusite-slow.log

Cada entrada mostra a pilha de chamadas no momento em que a requisição passou de 5 segundos. Na prática, o resultado quase sempre aponta para uma das três: consulta sem índice, chamada a API externa sem timeout, ou laço processando arquivo grande.

Chamada externa sem timeout é a mais insidiosa. Um parceiro que fica lento consome todos os seus workers em minutos, e o seu site cai por causa do sistema de outra pessoa. Todo cliente HTTP dentro da aplicação precisa de timeout explícito e curto.

Ver o estado em tempo real

pm.status_path = /fpm-status
location = /fpm-status {
    allow 127.0.0.1;
    deny all;
    fastcgi_pass unix:/run/php/meusite.sock;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
curl -s http://127.0.0.1/fpm-status

Duas linhas dessa saída valem por todo o resto. listen queue diferente de zero significa que há requisição esperando worker — é o sinal antecedente do 502. E max children reached com valor crescente é a confirmação de que o limite está sendo atingido de fato.

Se max children reached sobe e ainda há memória livre, aumente max_children. Se sobe e a memória está no limite, o caminho é reduzir o consumo por requisição ou aumentar a máquina — não adianta aumentar o número de workers sem RAM para sustentá-los.

OPcache: o ajuste de maior retorno

Sem OPcache, o PHP recompila cada arquivo a cada requisição. Ligá-lo costuma cortar o tempo de resposta pela metade e reduzir o consumo de CPU na mesma proporção.

; /etc/php/8.3/fpm/conf.d/10-opcache.ini
opcache.enable = 1
opcache.memory_consumption = 192
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 1
opcache.revalidate_freq = 2

Em produção com deploy controlado, validate_timestamps = 0 extrai o máximo — mas exige recarregar o FPM a cada implantação, senão o código novo simplesmente não entra em vigor. É uma troca consciente: mais desempenho, um passo obrigatório no deploy.

Confira se o cache não está estourando, o que anula o benefício:

php -r 'print_r(opcache_get_status(false)["opcache_statistics"]);'

oom_restarts maior que zero significa que a memória do OPcache é insuficiente.

Socket unix ou TCP

Uma decisão pequena com efeito mensurável. O PHP-FPM aceita conexão por socket unix ou por porta TCP, e o padrão da distribuição varia.

Socket unix é mais rápido — não passa pela pilha de rede — e mais seguro, porque o acesso é controlado por permissão de arquivo em vez de por firewall. É a escolha certa quando nginx e PHP-FPM estão na mesma máquina, que é o caso da maioria esmagadora.

TCP só faz sentido quando os dois estão em máquinas diferentes. Nesse caso, o PHP-FPM passa a escutar em rede, e aí vale tudo que se aplica a qualquer serviço exposto: escutar apenas no IP interno, restringir a origem e nunca deixar acessível pela internet.

; socket unix — preferível
listen = /run/php/meusite.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

; TCP, apenas se em máquinas separadas
; listen = 10.0.0.11:9000
; listen.allowed_clients = 10.0.0.10

O listen.owner precisa ser o usuário do nginx, e não o da aplicação. É a inversão que mais confunde: o socket é criado pelo FPM, mas quem precisa abrir o arquivo é o nginx. Errar isso produz o 502 com Permission denied no log — o mesmo código, causa completamente diferente.

O roteiro quando o 502 aparecer

# 1. o que o nginx diz exatamente
sudo tail -30 /var/log/nginx/error.log

# 2. o FPM está vivo?
systemctl status php8.3-fpm

# 3. há fila?
curl -s http://127.0.0.1/fpm-status | grep -E 'listen queue|max children'

# 4. memória sobrando?
free -h

# 5. o que trava
sudo tail -40 /var/log/php-fpm/meusite-slow.log

Essa sequência distingue os três cenários em poucos minutos: fila cheia com memória sobrando é max_children baixo; fila cheia com memória no limite é consumo por requisição; FPM parado é outro problema, e o diagnóstico do systemd cobre.

E vale lembrar que 502 também acontece por motivos que nada têm a ver com PHP — os erros de configuração do nginx como reverse proxy produzem o mesmo código, e conferir de que lado está o problema é sempre o primeiro passo.

Suba um servidor em minutos

Preço em real, suporte em português e dados no Brasil.

Criar conta