Imagina el reporte de bug que aterriza en staging un viernes por la tarde: un smoke test sin sesión iniciada se encuentra con los datos de cuenta de otro tester. Nadie tocó el código de autenticación. Lo que cambió fue el runtime, porque el equipo acaba de pasar una aplicación Symfony al modo worker de FrankenPHP. Aquí va mi postura, dicha desde el principio: ese bug no es un argumento contra FrankenPHP. Es el mejor argumento que existe para al menos probarlo. Durante dos décadas, el desmontaje por petición de PHP ha estado absorbiendo en silencio nuestros hábitos más descuidados, y creo que perder ese pase gratis es una de las cosas más sanas que le pueden pasar a un código maduro.
<?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)) {
}Un poco de contexto rápido para quien solo haya visto pasar el nombre. FrankenPHP incrusta el intérprete oficial de PHP directamente en Caddy mediante CGO y ejecuta tu código sobre un pool de hilos POSIX. Sin demonio PHP-FPM, sin socket FastCGI, un solo proceso sirviendo TLS, HTTP/2 y HTTP/3, ficheros estáticos y PHP en un único flujo de logs. En modo clásico se comporta como el montaje que ya conoces: cada petición arranca de cero, y puedes ponerlo debajo de una aplicación existente sin reescribir nada. El modo worker es la parte interesante. Tu aplicación arranca una vez, y luego se queda residente respondiendo petición tras petición desde el mismo proceso.
Ahora piensa en lo que el modelo tradicional ha estado haciendo por ti todo este tiempo. Cada propiedad estática, cada singleton memoizado, cada valor de configuración que metiste en algún rincón global quedaba triturado en cuanto salía la respuesta. Ese desmontaje era un conserje nocturno limpiando detrás de un código que nunca aprendió a limpiar solo. El modo worker despide al conserje. Los estáticos sobreviven. Los listeners de eventos que registras dentro de un manejador de peticiones se acumulan, uno más por petición. Un ID de tenant cacheado en una clase de utilidades sobrevive al tenant. Hasta las superglobales exigen leer la letra pequeña: FrankenPHP reinicia la mayoría entre peticiones, pero la documentación señala actualmente que $_ENV no se reinicia, así que los datos de entorno deben tratarse como estrictamente inmutables y no usarse jamás para colar valores por petición.
El artículo original en el que me apoyo muestra el fallo en once líneas, y merece que te quedes mirándolo. Una petición fija un ID de usuario en una propiedad estática. La siguiente no envía nada y aun así ve al usuario 42, porque la clase se quedó en memoria. Nada de eso es un defecto de FrankenPHP. Mantener el proceso vivo es exactamente de donde sale la mejora de velocidad, ya que el autoload de Composer, la construcción del contenedor y la del kernel ocurren una vez en lugar de en cada golpe. La integración con Laravel Octane trae una opción --max-requests y recicla los workers por defecto tras un número acotado de peticiones, lo que limita el crecimiento de memoria. Pero reciclar es un detector de humo. Te avisa de que algo se está quemando; no apaga el fuego en tu código.
Entonces, ¿por qué lo llamo detector de mentiras y no peligro? Porque cada defecto que saca a la luz ya era un defecto disfrazado. Si tu aplicación se comporta mal cuando el proceso vive más de una petición, casi seguro que tienes la misma podredumbre asomando en otros sitios: suites de tests inestables que pasan en aislamiento y fallan en secuencia, workers de colas que necesitan un reinicio nocturno, gráficas de memoria que solo saben subir. Llevas años ejecutando PHP de larga duración en tus consumidores y demonios. El modo worker solo aplica esa disciplina a la capa web, y los frameworks te ponen la mitad del camino: Octane instala el servidor FrankenPHP con dos comandos de artisan, y Symfony soporta el modelo worker de forma nativa desde 7.4, con un paquete PHP Runtime que cubre versiones anteriores.
El contraargumento honesto merece todo su peso. FrankenPHP requiere PHP thread-safe, y la lista de compatibilidad tiene agujeros: imap, newrelic y pcov no están soportadas hoy por hoy, e imagick llega con salvedades documentadas. Si New Relic sostiene tu historia de observabilidad, eso por sí solo puede cerrar la conversación hoy mismo. La documentación de rendimiento también empuja las cargas exigentes hacia imágenes Debian, porque PHP con hilos sobre musl, que es lo que usa Alpine, rinde apreciablemente peor y la diferencia es medible. Y si tu equipo opera una plataforma curtida de Nginx más PHP-FPM con años de dashboards, runbooks y reflejos de guardia detrás, cambiarla para que el diagrama de arquitectura quede más bonito es un mal negocio. Un monolito CRUD aburrido con tráfico modesto no va a notar nada de esto.
Aun así aterrizo donde empecé, por una razón que tiene poco que ver con el throughput. Ejecuta FrankenPHP en modo clásico y obtienes la consolidación operativa por sí sola: un binario, certificados automáticos, métricas de Prometheus para hilos ocupados, tiempo de petición y profundidad de cola, y se acabó hacer grep en dos servicios para reconstruir una petición fallida. Luego apunta un entorno de staging al modo worker y machácalo con peticiones intercaladas de distintos usuarios, tenants y locales. O aguanta, y tienes margen de latencia esperando a que lo reclames, o tiene fugas, y ahora posees un mapa preciso de cada lugar donde tu aplicación confunde estado global con estado de petición. Los dos resultados valen más que la tarde que cuestan.
Esto es lo que quiero saber de ti. ¿Has activado de verdad el modo worker contra un código con más de cinco años, y qué salió arrastrándose de ahí? ¿Un logger estático reteniendo un contexto de petición, un listener registrado diez mil veces, algo más raro? Y para quienes lo probaron y volvieron a PHP-FPM: ¿fueron los huecos en las extensiones, la experiencia de depuración, o simplemente los bugs de aislamiento costaron más de lo que pagaba el ahorro de arranque? Los comentarios están abiertos, y sospecho que las batallitas son mejores que cualquier benchmark.




Comentarios
Aún no hay comentarios — escribe el primero.
Inicia la conversación
Sin cuenta ni contraseña — introduce tu correo y te enviamos un enlace de acceso de un solo uso. ¿Primera vez? Todo se configura automáticamente.
Tu valoración se aplicará automáticamente al iniciar sesión.
Revisa tu bandeja de entrada
Hemos enviado un enlace de acceso a …. Ábrelo en este dispositivo — esta pestaña te conectará automáticamente.
¿No llega nada? Mira en la carpeta de spam — y marca el correo como «No es spam» para que la próxima vez llegue directo.