PHP-FPM ou worker persistente? O desempenho vem com memória de longo prazo

Por muito tempo, uma das melhores características operacionais do PHP foi também uma limitação de desempenho: cada requisição começa com um contexto limpo. O processo do PHP-FPM pode continuar vivo, mas o estado daquela execução é desmontado ao fim do request. Variáveis, objetos e pequenos vazamentos deixam de existir antes de o próximo usuário chegar.

Servidores e runtimes com workers persistentes mudam esse acordo. Laravel Octane, FrankenPHP, RoadRunner e Open Swoole conseguem carregar a aplicação uma vez e alimentar o mesmo worker com várias requisições. O bootstrap deixa de ser pago repetidamente. Em troca, a aplicação precisa lembrar que agora ela também tem memória.

FPM não inicia o PHP inteiro do zero

É comum resumir PHP-FPM como “um processo novo por requisição”, mas isso não descreve os pools normais. O gerenciador mantém processos trabalhadores e distribui requisições entre eles. O que recomeça é o ciclo da aplicação: arquivos de entrada são executados novamente, o container é montado e o estado daquele request é liberado no encerramento. OPcache ainda pode preservar bytecode compilado entre execuções.

Esse modelo isola bem erros acidentais. Uma variável estática que cresce durante uma requisição desaparece na limpeza. Uma dependência que guarda o objeto Request não o entrega ao usuário seguinte. O custo é reconstruir partes da aplicação que poderiam ser reaproveitadas.

O ganho dos workers persistentes

Ao manter framework, container e serviços em memória, o worker evita uma parcela do trabalho repetitivo. Aplicações com bootstrap pesado podem reduzir latência e aumentar throughput. Alguns runtimes acrescentam concorrência, WebSockets ou I/O assíncrona, mas são capacidades separadas: persistir a aplicação não torna automaticamente todo código PHP não bloqueante.

Esse detalhe importa porque benchmark de “requisições por segundo” pode misturar benefícios diferentes. Parte do ganho vem de não reconstruir a aplicação. Outra parte pode vir do servidor, do modelo de concorrência ou de conexões reaproveitadas. A única medida confiável é o comportamento da sua carga, com banco, cache e integrações reais.

O bug que atravessa requisições

No worker persistente, qualquer coisa guardada em escopo global, estático ou singleton pode sobreviver. O exemplo óbvio é um array estático que recebe dados a cada request e nunca é esvaziado. Os casos mais traiçoeiros são objetos que parecem configuração, mas capturam informação do usuário atual.

final class CurrentTenant
{
    public static ?string $id = null;
}

// Em um worker persistente, esquecer de limpar esse valor
// pode entregar o tenant anterior à próxima requisição.

A documentação do Laravel Octane alerta para injeção do container e do request em objetos singleton. O singleton pode ser criado no primeiro request e continuar apontando para aquela instância antiga. O mesmo raciocínio vale para locale, timezone, flags temporárias, handlers registrados dinamicamente e caches sem limite.

Conexão persistente também carrega estado

Reaproveitar uma conexão com banco ou Redis evita handshakes, mas uma conexão não é apenas um socket. Ela pode carregar uma transação aberta, uma configuração de sessão, uma tabela temporária ou um estado de autenticação. O pool precisa devolver conexões limpas e descartar as que ficaram em situação incerta.

  • Não guarde dados de request em propriedades estáticas ou singletons longevos.
  • Garanta commit ou rollback em todos os caminhos de execução.
  • Recarregue workers quando código ou configuração mudar.
  • Defina reciclagem após determinado número de requests como contenção, não como correção.
  • Monitore memória por worker e teste tráfego sequencial com usuários diferentes.

Quando a troca se paga

Uma API de alta frequência, um framework com bootstrap caro ou um serviço que precisa manter conexões podem aproveitar bastante o modelo persistente. Um site cujo tempo está concentrado no banco, em APIs externas ou em páginas cacheadas pode ganhar pouco e assumir uma disciplina nova sem retorno proporcional.

A migração pede testes de longa duração, não apenas um benchmark de cinco minutos. Envie requisições de tenants, idiomas e permissões diferentes para o mesmo worker. Force exceções no meio de transações. Observe a curva de memória. Reinicie processos durante carga. A pergunta não é só “ficou mais rápido?”, mas “o centésimo milésimo request continua isolado como o primeiro?”.

PHP-FPM continua sendo uma escolha previsível porque a limpeza do ciclo de request compra simplicidade. Workers persistentes compram desempenho ao retirar essa limpeza automática. Nenhum dos dois é atraso ou evolução por si só; são contratos operacionais diferentes.

Fontes