El recuento de visualizaciones es el primer número en el que confía un visitante. Les indica si un vídeo merece treinta segundos o treinta minutos de su tiempo. En TopVideoHub, ese número se mueve rápido. Un clip de tendencia puede acumular 40.000 visualizaciones en diez minutos. Si el contador de la página se congela, la sala se siente vacía. Los usuarios abandonan el sitio.

Hacer que ese número llegue al navegador suena trivial. No lo es. La primera solución a la que recurren la mayoría de los equipos es el polling. Es fácil de implementar y funciona bien en staging. Pero staging miente.

Cuando el polling se convierte en un ataque DDoS contra ti mismo

El equipo de TopVideoHub construyó un poller de JavaScript sencillo. Consultaba el último recuento de visualizaciones cada cinco segundos. En un entorno de pruebas con tres navegadores abiertos, se veía genial. En producción, colapsó la plataforma.

Ocho mil espectadores simultáneos refrescando cada cinco segundos generaron 1.600 solicitudes por segundo. Cada solicitud impactaba directamente en la base de datos. El retraso de replicación se disparó. Las réplicas de lectura sufrieron. Las capas de caché fueron omitidas. El equipo no estaba sirviendo vídeo; estaban sirviendo una carga autoinfligida.

El polling es inocente hasta que deja de serlo. Para paneles de administración o dashboards de poco tráfico, está bien. Para una página de un vídeo viral, es una bomba de relojería. El equipo necesitaba un canal persistente del servidor al navegador, pero no necesitaba la complejidad de un protocolo full-duplex.

Por qué SSE se adapta a los canales unidireccionales

Server-Sent Events (SSE) está diseñado precisamente para este tipo de problema: el servidor tiene los datos y el navegador solo necesita escuchar.

A diferencia de WebSockets, SSE funciona sobre HTTP puro. Eso importa más de lo que parece. No necesitas nuevas reglas de proxy, encabezados de upgrade o gimnasia de balanceadores de carga. Si tu servidor habla HTTP/1.1 o HTTP/2, SSE funciona. La depuración es indolora porque el flujo es simplemente texto. Puedes apuntar curl al endpoint y ver cómo los números pasan en tiempo real, lo cual es mejor que estar adivinando por qué un frame de socket binario salió mal.

El navegador se encarga de las partes tediosas de forma gratuita. Si la conexión se cae, SSE se reconecta automáticamente con el encabezado Last-Event-ID para que el servidor sepa dónde reanudar. La superficie de la API en JavaScript es mínima: creas un EventSource, adjuntas un manejador onmessage y listo.

El almacenamiento en caché es la verdadera arquitectura

El mayor error arquitectónico en los contadores en vivo es tratar cada conexión de navegador como un motivo para consultar la base de datos. Si 8.000 personas ven el mismo vídeo, ejecutar 8.000 consultas cada dos segundos es una locura. Tu base de datos no sobrevivirá al tráfico viral.

TopVideoHub resolvió esto con APCu, el caché de usuario y opcode en memoria de PHP. El flujo es sencillo. Un proceso en segundo plano —o un endpoint ligero consultado mediante un temporizador— escribe el recuento de visualizaciones actual en APCu una vez cada dos segundos. El endpoint de SSE, que miles de navegadores pueden mantener abierto, lee exclusivamente desde APCu.

El resultado: la base de datos recibe una sola consulta cada dos segundos, sin importar cuántos espectadores estén conectados. El caché se convierte en el amortiguador. APCu no es algo exótico. Viene con PHP, vive en la memoria compartida y lee más rápido que cualquier viaje de ida y vuelta por la red. Para un único número que cambia con frecuencia pero no instantáneamente, es la herramienta adecuada.

Si no estás usando APCu, Redis o Memcached también funcionan. El principio es el mismo: separa la ruta de lectura caliente de la base de datos.

Convencer a PHP, LiteSpeed y Cloudflare para que transmitan en streaming

PHP quiere terminar e irse a casa. Los servidores web quieren almacenar la salida en búfer y entregar una respuesta limpia. SSE necesita lo contrario: una conexión que permanezca abierta, enviando bytes a medida que llegan. Sin cuidado, tu "stream" llegará como un único bloque treinta segundos después, anulando el propósito.

Así es como TopVideoHub mantuvo el canal sin obstrucciones.

Elimina el buffering de salida. Al inicio del script de SSE, desactiva todas las capas de buffering que PHP pueda haber habilitado. Llama a ob_end_flush() si hay un búfer activo, y desactiva el flushing implícito con ob_implicit_flush(true) después de enviar tus encabezados.

Diles a los proxies que se retiren. Envía el encabezado X-Accel-Buffering: no. Nginx lo escucha. LiteSpeed lo escucha. Indica que la respuesta no debe almacenarse en búfer ni comprimirse en un bloque cacheable.

Establece una vida útil corta. Cada conexión SSE ocupa un worker de PHP. TopVideoHub limita los streams a 55 segundos. Cuando el temporizador llega al límite, el servidor envía un comentario final, cierra el stream y el navegador se reconecta automáticamente. Esa reconexión llega a un nuevo worker, evitando que un solo proceso se quede ocupando el recurso para siempre.

Ping para mantenerse vivo. Envía una línea de comentario —algo como : ping— cada veinte segundos. Los comentarios en SSE son ignorados por el manejador de mensajes del navegador, pero mantienen la conexión TCP activa. Los balanceadores de carga y las CDN suelen cerrar las conexiones inactivas después de treinta o sesenta segundos. Un simple salto de línea te salva de ese golpe.

Respeta la pestaña del usuario. Cuando un visitante minimice u oculte la pestaña, abandona la conexión. Escucha el evento visibilitychange en el navegador y llama a eventSource.close(). El servidor también debe detectar la desconexión del cliente y terminar el bucle. PHP puede verificar connection_aborted() dentro de un bucle. No permitas que las conexiones fantasma consuman workers para personas que se fueron hace diez minutos.

El límite infranqueable: Workers de PHP

SSE en PHP es honesto sobre sus límites. Cada conexión SSE abierta consume un worker de PHP. Si tu pool tiene cien workers, tienes cien streams. Punto final. No hay una solución asíncrona mientras estés dentro de un modelo de procesos de Apache o PHP-FPM. Puedes ajustar pm.max_children, pero la memoria y la CPU establecen el límite real.

Ese límite se alcanza rápido si también estás usando workers para cargas de páginas regulares, llamadas a APIs y generación de assets. Monitorea la saturación de tus workers con cuidado. Si tu endpoint de SSE empieza a encolarse porque todos los workers están bloqueados en streams de veinte minutos, todo tu sitio se volverá lento.

Cuando los números ya no encajen, muévete. Go es el siguiente paso habitual, aunque Rust, Node.js o Erlang pueden cumplir la misma función. Las goroutines de Go son la clave. Una goroutine cuesta unos pocos kilobytes. Puedes mantener decenas de miles de streams en hardware modesto sin despeinarte. La lógica central sigue siendo idéntica —leer de la caché, escribir en el socket— pero el runtime cambia de procesos pesados a hilos ligeros.

Sin embargo, no empieces por ahí. PHP te llevará sorprendentemente lejos. Valida el producto primero. Cuando la página de métricas muestre agotamiento de workers en lugar de sobrecarga de la base de datos, habrás superado el stack. Ese es un buen problema.

Conclusión

Los contadores en vivo no se tratan de tecnología pura. Se tratan de proteger tu base de datos de tus propios usuarios. Empieza con SSE porque es más sencillo de lo que parece. Usa caché de forma agresiva entre el stream y la base de datos para que el número de conexiones no se convierta en el número de consultas. Vigila tus límites de workers como un halcón. Y empieza de forma sencilla. PHP es suficiente hasta que deja de serlo, y para entonces sabrás exactamente por qué estás reescribiendo.

Para tu próximo proyecto:

  • Usa SSE cuando los datos fluyan en una sola dirección, del servidor al navegador.
  • Coloca una capa de caché frente a la base de datos. Una consulta cada pocos segundos es mejor que miles.
  • Limita las conexiones SSE a menos de un minuto y deja que el navegador se reconecte.
  • Cierra los streams cuando se oculte la pestaña. No mantengas conexiones inactivas consumiendo workers activos.
  • Monitorea el uso de workers de PHP. Cuando llegues al máximo, migra la capa de streaming a Go.