Todos os artigos
37 artigos · atualizado semanalmente Veja nossas Ferramentas
Todos os artigos
Comparativos

FrankenPHP, Swoole ou RoadRunner: o PHP que você zoava não existe mais

Comparação real dos runtimes PHP em 2026: worker mode, benchmarks de FrankenPHP, Swoole e RoadRunner, riscos de memory leak e qual escolher para produção.

COVER · Comparativos

FrankenPHP, Swoole ou RoadRunner: o PHP que você zoava não existe mais

Minha stack declarada é Python e Go, e durante anos PHP foi minha piada de retrospectiva. Aí o backend do Quick Tools acabou rodando em FrankenPHP — decisão de outro contexto, herança de arquitetura — e eu tive que parar de rir e começar a medir. O resultado: uma API que boota o framework uma vez, responde em milissegundos e faz deploy como um binário estático. Igualzinho ao que eu fazia em Go, só que com 20 anos de ecossistema atrás.

Esse artigo é sobre os três runtimes que mudaram esse jogo — FrankenPHP, Swoole e RoadRunner —, o que cada um faz de diferente, os números reais de benchmark (e por que você não deve confiar cegamente neles) e o preço que o worker mode cobra em troca do throughput.


O problema nunca foi o PHP — era o modelo de execução

A piada do "PHP é lento" sempre mirou no alvo errado. O interpretador do PHP 8 com JIT é rápido. O que era lento é o modelo shared-nothing: a cada request HTTP, o PHP-FPM entrega a requisição a um processo que carrega o autoloader, lê a configuração, monta o container de injeção de dependência, registra os service providers do framework, processa a request — e joga tudo fora. Na request seguinte, repete do zero.

Para um script de 200 linhas em 2005, esse modelo era uma feature: impossível vazar estado, impossível vazar memória, crash de um processo não derruba nada. Para um Laravel ou Symfony de 2026, é um imposto fixo de dezenas de milissegundos de bootstrap pago em toda request, antes de qualquer linha do seu código rodar. OPcache elimina o custo de parsing, mas não o de inicialização do framework.

Node.js, Go e Python (com Gunicorn/Uvicorn) nunca pagaram esse imposto: a aplicação sobe uma vez e fica residente em memória atendendo requests. A novidade dos últimos anos é que o PHP agora também faz isso — e de três jeitos diferentes.

Worker mode: a aplicação que não morre a cada request

A ideia comum aos três runtimes é o worker mode: o framework boota uma vez e entra num loop que recebe requests já com tudo quente — container montado, rotas compiladas, conexões abertas. No FrankenPHP, o esqueleto de um worker é literalmente isso:

<?php
// public/worker.php
ignore_user_abort(true);

$app = bootApplication(); // roda UMA vez, não a cada request

$handler = static function () use ($app) {
    $response = $app->handle(Request::fromGlobals());
    $response->send();
};

while (frankenphp_handle_request($handler)) {
    // limpeza entre requests, se necessário
}

Na prática você raramente escreve esse loop na mão: o Laravel Octane e o runtime do Symfony abstraem isso para os três runtimes. Mas entender que existe um while ali embaixo importa — porque tudo que vaza dentro desse loop, vaza para a próxima request. Volto nisso adiante, porque é onde mora o perigo.

O ganho, dependendo do peso do seu bootstrap, vai de 2× a 10× de throughput. Não porque o PHP ficou mais rápido — porque ele parou de refazer o mesmo trabalho centenas de vezes por segundo.

FrankenPHP: o servidor e o PHP viram uma coisa só

O FrankenPHP embute o interpretador PHP dentro do Caddy, o servidor web escrito em Go. Não tem nginx na frente, não tem FPM atrás: o servidor HTTP e o runtime PHP são o mesmo processo. Isso traz de graça o que o Caddy já faz bem — HTTPS automático, HTTP/2 e HTTP/3 nativos, early hints (HTTP 103) — e elimina uma camada inteira da sua infraestrutura.

O argumento mais forte na prática é operacional: deploy de um binário estático ou de uma imagem Docker mínima. Sem orquestrar nginx + FPM + supervisor, sem sincronizar timeout de proxy com timeout de worker. Para container em Kubernetes ou qualquer ambiente cloud-native, é o caminho de menor atrito. E roda como um único processo multi-thread, o que dá o menor footprint de memória dos três — relevante quando seu pod tem limite de 256MB.

O ponto fraco é a idade: é o runtime mais novo, com a menor base de conhecimento acumulado da comunidade. Quando algo quebra às 23h, tem menos issue antiga no GitHub com a sua cara. Isso está mudando rápido — o projeto foi adotado pela organização oficial do PHP — mas em 2026 ainda é um fator.

Swoole: corrotinas de verdade, com letras miúdas

O Swoole é diferente dos outros dois por natureza: não é um servidor na frente do PHP, é uma extensão em C que transforma o PHP num runtime assíncrono orientado a eventos, com corrotinas. O detalhe elegante é que os coroutine hooks interceptam as funções de I/O bloqueantes do PHP — file_get_contents, PDO, Redis — e as tornam cooperativas sem você reescrever nada com async/await:

use function Swoole\Coroutine\run;
use function Swoole\Coroutine\go;

run(function () {
    // as duas chamadas executam concorrentemente:
    // cada corrotina cede o controle enquanto espera I/O
    go(fn () => file_get_contents('https://api-pagamentos.example.com/status'));
    go(fn () => file_get_contents('https://api-frete.example.com/cotacao'));
});

Quando a sua carga é dominada por espera de I/O — agregação de APIs externas lentas, WebSockets com milhares de conexões simultâneas, banco com latência alta — o Swoole faz coisas que os outros dois não fazem, porque um worker atende outras requests enquanto espera.

As letras miúdas: é uma extensão C que precisa estar instalada (esqueça hosting genérico), o tooling de debug tem incompatibilidades históricas, e o seu código precisa ser seguro para corrotinas — uma lib que guarda estado em variável estática vira condição de corrida. E tem o ponto que quase ninguém fala: se o seu I/O é rápido, o scheduling de corrotinas vira overhead puro. Os números abaixo mostram isso de forma constrangedora.

RoadRunner: o veterano que ganha benchmark

O RoadRunner é um servidor de aplicação em Go que mantém um pool de workers PHP vivos e conversa com eles por pipes. É a opção mais madura dos três no Octane e a que tem mais histórico de produção acumulado.

A diferença filosófica: RoadRunner não quer ser só um servidor HTTP. Ele traz um sistema de plugins — filas (jobs), key-value store, servidor gRPC, integração com Temporal para workflows — que o transforma numa plataforma de aplicação. Se o seu monolito precisa de fila e gRPC além de HTTP, o RoadRunner resolve no mesmo binário o que os outros exigiriam de infraestrutura adicional.

O modelo de worker é o mais conservador dos três: processos PHP comuns, residentes em memória, sem corrotinas. E é exatamente essa simplicidade que ganha benchmark.

Os números — e por que você não deve confiar cegamente neles

O benchmark mais recente que encontrei (PHP 8.5, Laravel 11, 100 conexões concorrentes, workload de CPU + uma query rápida no MySQL):

Runtime Req/s P99 vs PHP-FPM
PHP-FPM (baseline) 374,6 2753ms
RoadRunner 792,4 1618ms +111,6%
FrankenPHP 375,9 3127ms +0,3%
Swoole 371,1 3084ms −0,9%

Lendo só a tabela, a conclusão é "RoadRunner ganha, Swoole perde até pro FPM". Lendo a metodologia, a história muda:

  • O Swoole perdeu porque a query do teste levava menos de 1ms. Sem espera de I/O, corrotina não tem o que sobrepor — só paga o custo do scheduling. Troque o SELECT 1 por uma chamada de 200ms a uma API externa e a tabela inverte dramaticamente.
  • O FrankenPHP empatou com o FPM em parte porque o ApacheBench só fala HTTP/1.0 — o multiplexing de HTTP/2/3, que é metade do argumento dele, ficou fora do teste. E o footprint de memória dele foi o menor de todos, o que a tabela de RPS não mostra.
  • O RoadRunner ganhou porque o teste mede exatamente o que ele otimiza: eliminar o custo de bootstrap em requests curtas. É um resultado real, mas é um perfil de carga.

A lição não é "benchmarks mentem". É que benchmark responde à pergunta que o autor fez, não à sua. Antes de migrar, rode 30 minutos de load test com a sua rota mais quente. É a diferença entre escolher runtime e escolher gráfico.

O preço do worker mode: estado que vaza entre requests

Aqui está a parte que os posts de hype omitem. O modelo shared-nothing do FPM era um colchão de segurança: vinte anos de código PHP — incluindo as libs que você usa — foram escritos assumindo que tudo morre no fim da request. No worker mode, essa premissa quebra:

  • Propriedade estática ou singleton que acumula dados vira memory leak. Em FPM ninguém percebia; num worker que atende 100 mil requests, o processo incha até o OOM killer agir.
  • Estado de um usuário vazando para outro. O FrankenPHP teve um advisory de segurança exatamente sobre isso: $_SESSION não era resetada entre requests no worker mode. O clássico equivalente em Symfony é o EntityManager do Doctrine carregando entidades da request anterior.
  • Conexões e caches com ciclo de vida errado — o que era "por request" virou "para sempre" sem ninguém decidir isso.

As mitigações são conhecidas: use Octane ou o runtime do Symfony (que resetam o container entre requests em vez de confiar na sua disciplina), configure reciclagem de workers após N requests (MAX_REQUESTS no FrankenPHP, max_jobs no RoadRunner) como rede de proteção contra leaks, e monitore a memória por worker — não a do host. E audite as libs menos famosas da sua árvore de dependências: framework moderno é worker-safe; aquela lib de 2017 que você usa para gerar boleto, provavelmente não.

Worker mode te dá throughput e cobra em disciplina. O trade é bom — mas é um trade, não mágica.

Escolha pelo seu I/O, não pelo benchmark

Depois de meses com FrankenPHP em produção e dos números acima, minha matriz de decisão ficou assim: API HTTP stateless em container, projeto novo — FrankenPHP, pelo deploy de binário único e o menor footprint. Monolito web + API que também precisa de filas ou gRPC, time com CI maduro — RoadRunner, pela maturidade e pelos plugins. WebSockets em escala ou carga dominada por I/O lento de terceiros — Swoole, que é o único dos três com concorrência real dentro do worker. E se o seu site atende 50 requests por minuto sem reclamar, PHP-FPM continua sendo uma resposta correta — migrar de runtime por hype é trocar um problema que você não tem por vários que você não conhece.

O ponto maior: a crítica de 2015 ao PHP morreu de velhice. O modelo de execução que a justificava não é mais obrigatório — virou uma escolha, com três alternativas sólidas. Se faz tempo que você não olha para esse ecossistema, os números acima são um bom motivo para olhar de novo.


Nota: o conteúdo editorial acabou aqui. O que vem abaixo é uma indicação de ferramenta relacionada ao tema do post.


Ferramenta relacionada

Se você vai fazer load test ou debugar respostas de uma API PHP nesses runtimes, o JSON Formatter ajuda a inspecionar e validar os payloads de resposta — formata, destaca erros de sintaxe e roda 100% no navegador, sem enviar seus dados para servidor nenhum.

RD
Autor
Rafael Duarte
Desenvolvedor backend com passagem por fintech e SaaS B2B — trabalhou em times que escalaram APIs de zero a milhões de requisições. Carrega cicatrizes de produção suficientes para ter opiniões fortes sobre ferramentas, padrões e decisões de arquitetura. Não é acadêmico: leu a RFC do UUID quando precisou escolher entre v4 e v7 para uma tabela de alta escrita.
Ver perfil