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
commitourollbackem 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.
Defina um reset explícito por request
O framework pode limpar parte do container, mas serviços próprios continuam sob sua responsabilidade. Crie hooks de início e fim para locale, contexto de tenant, caches por request, handlers e transações. O reset precisa rodar também quando uma exceção interrompe o fluxo.
try {
$context->begin($request);
return $kernel->handle($request);
} finally {
$connections->rollbackOpenTransactions();
$context->reset();
$requestCache->clear();
}
Teste duas identidades no mesmo worker, force erro depois de abrir transação e confirme que a segunda requisição não herda locale, permissão nem conexão suja. Uma readiness probe pode retirar workers que falham na inicialização; reciclagem por número de requests contém vazamento enquanto a causa é corrigida.
Observe a curva, não apenas o pico
- RSS e heap por worker ao longo de milhares de requests;
- tempo de resposta antes e depois da reciclagem;
- conexões abertas, transações e handlers registrados;
- shutdown gracioso durante deploy, sem abandonar request em andamento;
- diferença entre cache de bytecode do OPcache e objetos vivos da aplicação.
O PHP 8.6 RC3 também merece um cenário com conexões persistentes: a linha 8.6 passa a usar COM_RESET_CONNECTION em versões suportadas de MySQL e MariaDB. Isso melhora a limpeza, mas não corrige estado guardado pela aplicação.

