Imagine le rapport de bug qui tombe en staging un vendredi après-midi : un test de fumée déconnecté se voit servir les données du compte d'un autre testeur. Personne n'a touché au code d'authentification. Ce qui a changé, c'est le runtime, parce que l'équipe vient de basculer une appli Symfony sur le mode worker de FrankenPHP. Ma position, annoncée d'emblée : ce bug n'est pas un argument contre FrankenPHP. C'est le meilleur argument pour au moins l'essayer. Depuis deux décennies, le démontage par requête de PHP absorbe en silence nos habitudes les plus négligentes, et je pense que perdre ce passe-droit est l'une des choses les plus saines qui puissent arriver à un codebase mature.

worker.php
<?php
// Deliberately broken: static state outlives the request.
final class RequestContext
{
    public static ?int $userId = null;
}

$handler = static function (): void {
    if (isset($_GET['user_id'])) {
        RequestContext::$userId = (int) $_GET['user_id'];
    }
    echo 'Current user: ' . (RequestContext::$userId ?? 'guest');
};

while (frankenphp_handle_request($handler)) {
}

Petit rappel pour ceux qui n'ont vu passer le nom que de loin. FrankenPHP embarque l'interpréteur PHP officiel directement dans Caddy via CGO et fait tourner ton code sur un pool de threads POSIX. Pas de démon PHP-FPM, pas de socket FastCGI, un seul processus qui sert TLS, HTTP/2 et HTTP/3, les fichiers statiques et PHP dans un flux de logs unique. En mode classique, il se comporte comme la config que tu connais déjà : chaque requête démarre à neuf, et tu peux le glisser sous une appli existante sans rien réécrire. Le mode worker, c'est la partie intéressante. Ton application démarre une fois, puis reste résidente et répond requête après requête depuis le même processus.

Maintenant, pense à ce que le modèle traditionnel faisait pour toi depuis le début. Chaque propriété statique, chaque singleton mémoïsé, chaque valeur de config fourrée quelque part en global était déchiquetée dès que la réponse partait. Ce démontage, c'était un concierge de nuit qui nettoyait derrière du code qui n'a jamais appris à ranger. Le mode worker licencie le concierge. Les statiques survivent. Les event listeners que tu enregistres dans un handler de requête s'empilent, un de plus par requête. Un ID de tenant mis en cache dans une classe helper survit au tenant. Même les superglobales méritent qu'on lise les petites lignes : FrankenPHP en réinitialise la plupart entre les requêtes, mais la doc précise actuellement que $_ENV n'est pas réinitialisée, donc les données d'environnement doivent être traitées comme strictement immuables et ne jamais servir à faire passer en douce des valeurs propres à une requête.

L'article source sur lequel je brode montre la panne en onze lignes, et ça vaut la peine de s'y arrêter. Une requête pose un ID utilisateur sur une propriété statique. La requête suivante n'envoie rien et voit quand même l'utilisateur 42, parce que la classe est restée en mémoire. Rien de tout ça n'est un défaut de FrankenPHP. Garder le processus vivant, c'est exactement de là que vient le gain de vitesse, puisque l'autoload Composer, la construction du conteneur et celle du kernel se font une fois au lieu de se répéter à chaque hit. L'intégration Octane de Laravel fournit une option --max-requests et recycle les workers par défaut après un nombre borné de requêtes, ce qui plafonne la dérive mémoire. Mais le recyclage, c'est un détecteur de fumée. Il te dit que quelque chose brûle, il n'éteint pas l'incendie dans ton code.

Alors pourquoi je parle de détecteur de mensonges plutôt que de danger ? Parce que chaque défaut qu'il fait remonter était déjà un défaut déguisé. Si ton appli déraille quand le processus vit plus longtemps qu'une requête, tu as presque certainement la même pourriture ailleurs : des suites de tests capricieuses qui passent isolément et échouent en séquence, des workers de queue qui réclament un redémarrage nocturne, des courbes mémoire qui ne font que monter. Tu fais tourner du PHP longue durée depuis des années dans tes consumers et tes démons. Le mode worker applique simplement cette discipline à la couche web, et les frameworks font la moitié du chemin : Octane installe le serveur FrankenPHP en deux commandes artisan, et Symfony supporte le modèle worker nativement depuis la 7.4, avec un package PHP Runtime pour couvrir les versions plus anciennes.

Le contre-argument honnête mérite tout son poids. FrankenPHP exige un PHP thread-safe, et la liste de compatibilité a des trous : imap, newrelic et pcov ne sont pas supportés à ce jour, et imagick vient avec des réserves documentées. Si New Relic est un pilier de ton observabilité, ça peut clore la discussion dès aujourd'hui. La doc de performance oriente aussi les charges exigeantes vers les images Debian, parce que le PHP threadé sur musl, qu'Alpine utilise, tourne sensiblement plus lentement. Et si ton équipe exploite une plateforme Nginx plus PHP-FPM durcie, avec des années de dashboards, de runbooks et de réflexes d'astreinte derrière, la remplacer pour rendre le schéma d'architecture plus joli est un mauvais deal. Un monolithe CRUD tranquille à trafic modeste ne sentira rien de tout ça.

J'atterris quand même là où j'ai commencé, pour une raison qui a peu à voir avec le débit. Fais tourner FrankenPHP en mode classique et tu obtiens déjà la consolidation opérationnelle à elle seule : un binaire, des certificats automatiques, des métriques Prometheus pour les threads occupés, le temps de requête et la profondeur de file, fini le grep dans deux services pour reconstituer une requête en échec. Ensuite, pointe un environnement de staging sur le mode worker et matraque-le avec des requêtes entrelacées pour différents utilisateurs, tenants et locales. Soit ça tient, et tu as une marge de latence qui n'attend que d'être réclamée, soit ça fuit, et tu possèdes désormais une carte précise de chaque endroit où ton application confond état global et état de requête. Les deux issues valent plus que l'après-midi qu'elles coûtent.

Voilà ce que je veux savoir de toi. As-tu vraiment activé le mode worker sur un codebase de plus de cinq ans, et qu'est-ce qui en est sorti ? Un logger statique qui retient un contexte de requête, un listener enregistré dix mille fois, quelque chose de plus bizarre encore ? Et pour ceux qui ont essayé puis sont revenus à PHP-FPM : c'était les manques côté extensions, l'expérience de debug, ou les bugs d'isolation ont-ils simplement coûté plus cher que ce que rapportaient les économies de bootstrap ? Les commentaires sont ouverts, et je soupçonne que les récits de guerre valent mieux que n'importe quel benchmark.