El mito del «millón de peticiones por segundo» en PHP: la ingeniería real detrás de un API REST a escala masiva

En el mundo del rendimiento, pocas cifras suenan tan rotundas como 1.000.000 de peticiones por segundo (RPS). Se repite en charlas, titulares y debates de arquitectura, pero casi siempre con un matiz que cambia toda la historia: ese millón rara vez llega ?de golpe? a los servidores de aplicación. Lo que permite alcanzar volúmenes de ese calibre no es un framework milagroso ni un ajuste oscuro del kernel, sino una combinación de caché en el edge (CDN), diseño de endpoints cacheables, colas para desacoplar escrituras, motores de búsqueda especializados como Elasticsearch y runtimes PHP de proceso persistente.

La foto completa se parece más a una autopista con peajes inteligentes que a un único motor empujando más fuerte. Y en esa autopista, PHP 8+ puede jugar un papel relevante si se coloca donde mejor rinde: sirviendo lógica de negocio rápida, validación, autenticación y orquestación, mientras el sistema reparte el trabajo pesado en capas.

RPS no es rendimiento ?de la app?: es rendimiento del sistema

El dato de RPS, por sí solo, engaña. Dos sistemas pueden presumir del mismo número y ser radicalmente distintos:

? Un API que devuelve un JSON idéntico y cacheable puede escalar a niveles muy altos si el CDN responde desde memoria cerca del usuario.
? Un endpoint que calcula precios personalizados, revisa permisos y consulta varias fuentes por petición puede colapsar con cifras muy inferiores si no se diseña para evitar trabajo repetido.

La lección que aplican los equipos que realmente operan a escala es sencilla: primero se reduce el número de peticiones que llegan a origen. Solo después se optimiza la aplicación.

La arquitectura que hace posible el salto: de fuera hacia dentro

1) CDN/Edge caching: el ?acelerador? que convierte un API normal en uno masivo

Para aspirar a 1.000.000 RPS, la primera pregunta no es ?¿qué runtime uso??, sino ?¿qué porcentaje de tráfico puedo convertir en cache hit??. En un escenario realista, el 90?99% de muchas rutas de lectura (GET) debería resolverse sin tocar PHP.

Claves prácticas que suelen marcar la diferencia:

? Cache-Control bien definido: TTL en edge (s-maxage), revalidación, y directivas tipo stale-while-revalidate o stale-if-error para absorber picos y fallos del origen.
? Caché por código de estado: cachear 200, pero también 404 o 301 cuando tiene sentido (reduce tormentas de peticiones).
? Evitar respuestas ?uncacheable? por accidente: cookies, cabeceras de autorización o private/no-cache pueden forzar bypass si no se controlan.
? Request collapsing / revalidación: cuando el contenido está ?stale?, conviene que el CDN no dispare miles de revalidaciones al origen a la vez, sino que colapse peticiones.

En medios de programación se habla mucho de ?optimizar PHP?, pero en producción el gran salto suele venir de aquí: convertir el API en algo cacheable por diseño, no por casualidad.

Diseño de endpoints para que el CDN trabaje a favor

? GET públicos y estables: cacheables (ideal).
? GET con parámetros: cacheables si la clave de caché está controlada (y se limitan combinaciones).
? GET autenticados: normalmente no cacheables en CDN salvo estrategias por token/rol (costosas). Mejor cache distribuida (Redis) y tiempos de respuesta bajos.
? POST/PUT/PATCH: la regla general es no cachear, pero sí responder rápido y delegar trabajo.
 2) Runtime PHP persistente: adiós al «arranque por petición»

En una pila clásica Nginx + PHP-FPM, cada request repite costes: bootstrap del framework, carga de contenedor, autoload, configuración? Para un API pequeño puede bastar, pero cuando se busca exprimir latencia y throughput, la tendencia es clara: procesos persistentes que ?arrancan una vez? y procesan muchas peticiones.

En 2025, tres opciones se citan una y otra vez en proyectos serios:

RoadRunner: workers PHP gestionados por un servidor en Go

RoadRunner actúa como servidor de aplicaciones y gestor de procesos: mantiene workers PHP vivos y les entrega peticiones HTTP/HTTPS, incluso con soporte moderno (HTTP/2 h2c y HTTP/3). Este enfoque reduce el coste por request y permite escalar con una estrategia más propia de ?application servers? que de CGI tradicional.

Ejemplo conceptual de configuración de pool en .rr.yaml:

 

http:
 address: 0.0.0.0:8080
 pool:
   num_workers: 64
   max_jobs: 5000
   supervisor:
     max_worker_memory: 256

La idea no es ?poner 64 y listo?, sino ajustar workers a núcleos, latencias de I/O y patrón de carga.

Swoole / OpenSwoole: modelo event-driven con corutinas

Swoole y OpenSwoole llevan años empujando una propuesta clara: I/O asíncrona, corutinas y servidores embebidos para reducir latencia bajo concurrencia alta. Es potente, pero obliga a disciplina: un proceso largo en PHP no se comporta como el PHP ?clásico?, y el control del estado en memoria se convierte en tema central.

FrankenPHP: servidor moderno sobre Caddy con ?worker mode?

FrankenPHP ha ganado presencia por combinar una experiencia moderna de servidor (HTTP/2, HTTP/3, configuración estilo Caddy) con un modo worker que mantiene la aplicación cargada en memoria. Además, se integra con ecosistemas como Symfony Runtime y con Laravel Octane.

3) Laravel Octane y Symfony Runtime: el puente ?práctico? hacia procesos largos

No todo el mundo quiere reescribir su aplicación para un runtime nuevo. Aquí aparecen dos piezas que en medios de programación se han convertido en ?puentes? hacia alto rendimiento:

? Laravel Octane permite ejecutar Laravel en servidores de alto rendimiento (RoadRunner, Swoole, OpenSwoole o FrankenPHP), manteniendo la app en memoria y alimentándola de peticiones.
? Symfony Runtime busca desacoplar el arranque de la app de estados globales, facilitando ejecución en entornos persistentes (incluyendo FrankenPHP).

El mensaje técnico que se repite es el mismo: más rendimiento, sí, pero a cambio de asumir que hay memoria viva entre peticiones. Y eso exige higiene.

El gran enemigo: fugas de memoria y estado compartido

En runtimes persistentes, los bugs cambian de forma:

? Un singleton mal diseñado puede ?recordar? cosas de un usuario a otro.
? Un contenedor que acumula referencias puede crecer lentamente hasta matar el worker.
? Caches internas sin TTL pueden volverse bombas de RAM.

Por eso se imponen políticas como:

? reinicio controlado de workers por número de peticiones (max_jobs) o por memoria,
? limpieza de estado por request,
? timeouts agresivos en clientes (Redis/Elasticsearch/RabbitMQ),
? métricas de memoria y reinicios visibles.

Redis y microcaché: la capa que salva al origen incluso cuando el CDN no ayuda

Cuando el CDN no puede cachear (personalización, auth, variación por usuario), el siguiente escalón suele ser:

1. microcaché local (por proceso o APCu) para TTLs muy cortos,
2. Redis como caché distribuida,
3. y solo después búsquedas/DB.

Patrones que suelen funcionar a gran escala:

? cache-aside (buscar en caché y rellenar en miss),
? soft TTL: servir algo ?aceptable? mientras se regenera en background,
? anti-stampede: locks por clave o ?single flight? para que un miss no dispare miles de recomputaciones simultáneas.

Elasticsearch: búsqueda y agregaciones, con indexación desacoplada

Elasticsearch brilla cuando se usa como lo que es: motor de búsqueda. En un API de alto tráfico, el error típico es pedirle demasiado en tiempo real, con escrituras sincronizadas y refresh agresivo.

En pipelines serios, la receta se repite:

? El API no indexa en caliente en cada request de usuario.
? Publica un evento (cola).
? Workers de ingesta hacen bulk indexing.
? Se ajusta index.refresh_interval según necesidades (más alto o desactivado temporalmente durante cargas masivas).
? Se controla el coste de búsquedas populares con caché.

Esto reduce latencias del usuario y evita que la búsqueda se convierta en cuello de botella por refresh constantes.

RabbitMQ: el seguro de vida para las escrituras (y para la resiliencia)

Cuando un API quiere aguantar picos sin romperse, lo primero que intenta es quitar trabajo del camino crítico. RabbitMQ aparece justo ahí: separar ?recibo la intención del usuario? de ?procesaré esto con garantías?.

Patrón típico para endpoints de escritura:

? Validación + auth rápida.
? Persistencia mínima (si aplica).
? Publicación a RabbitMQ.
? Respuesta 202 Accepted con request_id.
? Procesamiento asíncrono por consumidores (con reintentos, idempotencia y DLQ si se necesita).

En entornos donde la durabilidad importa, también se debate el uso de colas más robustas (como quorum queues), con la consecuencia obvia: seguridad a cambio de coste. Esa decisión depende de negocio, no de moda.

«Deep system tuning»: lo que se ajusta cuando la arquitectura ya está bien

Una vez que el sistema está bien dividido (CDN + cachés + colas + runtimes persistentes), llega el momento de ?apretar? la máquina. Ahí aparecen clásicos:

? límites de descriptores de fichero (ulimit -n),
? colas de red y backlog (para evitar drops en picos),
? rango de puertos efímeros en clientes que abren muchas conexiones salientes,
? timeouts y keep-alive bien calibrados para no crear tormentas de conexiones,
? control de conntrack si hay NAT/firewall pesado.

En un medio de programación conviene subrayar algo: no existe un sysctl mágico. El tuning vale lo que vale el diagnóstico: si el cuello está en caché, tocar TCP no arregla nada; si el problema es saturación de conexiones, optimizar PHP tampoco.

Un blueprint «publicable» y repetible

Lecturas (GET) a escala

? CDN con TTL y revalidación.
? Redis para respuestas con personalización o lógica de negocio.
? Elasticsearch para búsquedas, protegido con caché en consultas populares.
? ETags/Last-Modified cuando el contenido lo permite.

Escrituras (POST/PUT) sin dolor

? Respuesta rápida (200/202).
? RabbitMQ como columna vertebral.
? Workers para persistencia, indexación y tareas costosas.
? Idempotencia por request_id (evita duplicados ante reintentos).

PHP 8+ como núcleo de lógica, no como cuello de botella

? RoadRunner / Swoole / OpenSwoole / FrankenPHP (según equipo y stack).
? OPcache y preloading para reducir latencia base.
? Políticas de reinicio de workers para evitar degradación con el tiempo.

Preguntas frecuentes

¿Es realmente posible servir 1.000.000 RPS con un API en PHP?

Sí, si el sistema está diseñado para que la mayoría de peticiones se resuelvan en el CDN/edge y el origen solo atienda una fracción (cache misses, auth, escrituras). Pretender 1.000.000 RPS sostenidos golpeando directamente a PHP suele ser un planteamiento irreal o prohibitivamente caro.

¿Qué conviene elegir para un API: RoadRunner, Swoole u FrankenPHP?

Depende del contexto: RoadRunner destaca por su modelo de workers gestionados y un enfoque muy ?servidor de aplicaciones?; Swoole/OpenSwoole por su modelo asíncrono y corutinas; FrankenPHP por su integración con Caddy y su worker mode, especialmente atractivo con Symfony Runtime o Laravel Octane. La clave es asumir procesos persistentes y controlar estado/memoria.

¿Cómo se integra Elasticsearch sin penalizar la latencia del API?

Separando lecturas de indexación: el API responde y encola eventos, y procesos de ingesta hacen bulk indexing con ajustes de refresh adecuados. Además, cachear búsquedas repetidas (CDN/Redis) evita recalcular consultas populares.

¿Qué patrón evita que las escrituras tiren el sistema cuando hay picos?

El patrón ?aceptar rápido y procesar en background?: POST ? validar ? publicar en RabbitMQ ? responder 202 ? consumidores procesan con reintentos e idempotencia. Es la base de muchos sistemas que necesitan absorber picos sin convertir cada request en una operación pesada.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP