Índice de contenido
- Ventajas y Limitaciones de SSE
- Ventajas
- Limitaciones
- Casos de Uso e Implementación Eficiente
- Mecanismo de Salida: Buffers y Flujo de Datos en PHP
- Estructura del Backend y Control de Conexión
- Recepción del Flujo en el Cliente (JavaScript / Vue)
- 1. ¿Qué es Server-Sent Events?
- Ventajas de SSE
- Limitaciones
- SSE vs WebSockets
- 2. Arquitectura del flujo
- 3. Backend — Controlador Laravel
- Puntos clave del controlador
- 4. Backend — Ruta
- 5. Frontend — Componente Vue 3
- Puntos clave del componente
- 6. Router y navegación
- 7. Tests (Pest PHP)
- 8. Verificación con curl
- 9. Notas para producción
- Worker blocking
- Rate limiting
- Autenticación
- Escalabilidad
- 10. Archivos creados o modificados
- Pasos para probar
Los Server-Sent Events (SSE) son una tecnología basada en el protocolo HTTP estándar que permite al servidor enviar actualizaciones en tiempo real hacia el cliente a través de una conexión abierta. A diferencia de los WebSockets, no requiere un protocolo específico ni un servidor independiente de mensajería (como Pusher o Reverb); se puede utilizar directamente en la infraestructura existente.
En esta arquitectura, el cliente se construye con JavaScript (por ejemplo, utilizando la reactividad de Vue) y el backend se encarga de emitir el flujo de eventos (usando frameworks como Laravel, Django, FastAPI, Express o cualquier entorno en Node.js).
Ventajas y Limitaciones de SSE
Para determinar cuándo implementar SSE en lugar de WebSockets, es necesario considerar sus características principales:
Ventajas
- Protocolo estándar: Funciona directamente sobre HTTP/HTTPS sin configuraciones especiales en el servidor.
- Reconexión automática: Los navegadores modernos gestionan la reconexión de forma nativa si la señal se interrumpe.
- Simplicidad de desarrollo: Formateo y procesamiento sencillo de datos (habitualmente en formato JSON).
Limitaciones
- Unidireccionalidad: La comunicación fluye exclusivamente del servidor al cliente. Si el cliente necesita enviar datos, debe realizar una petición HTTP tradicional (POST, PUT, etc.).
- Límite de conexiones: Bajo HTTP/1.1, los navegadores limitan las conexiones concurrentes a un máximo de 6 por dominio.
- Frecuencia de emisión: No está diseñado para aplicaciones de ultra alta frecuencia (como videojuegos en tiempo real), siendo ideal para intervalos de un segundo o superiores.
Casos de Uso e Implementación Eficiente
SSE es idóneo para paneles de notificaciones, barras de progreso para tareas pesadas en segundo plano o sistemas de actualizaciones en vivo. Dado el límite de conexiones concurrentes en servidores compartidos o bajo HTTP/1.1, se recomienda gestionar la apertura y cierre del flujo de forma consciente:
- Conexión bajo demanda: Abrir la conexión solo cuando el usuario interactúe con el módulo (por ejemplo, al abrir el menú de notificaciones o la interfaz de un chat).
- Sondeo por intervalos (Polling distribuido): Activar la conexión durante lapsos breves cada cierto tiempo (por ejemplo, cada 5 o 10 minutos) para verificar si hay nuevos eventos y cerrarla inmediatamente después.
Mecanismo de Salida: Buffers y Flujo de Datos en PHP
Para transmitir datos de forma progresiva sin esperar a que finalice la ejecución total del script, es necesario manipular el sistema de almacenamiento en búfer de PHP y del servidor web mediante ob_flush() y flush().
El flujo técnico de transmisión de la salida opera en dos capas:
ob_flush()(Nivel de Aplicación): Vacía el búfer de salida propio del interprete de PHP.flush()(Nivel de Servidor): Fuerza el envío de los datos almacenados en el búfer del servidor web hacia el navegador del cliente.
Sin la llamada secuencial a estas dos funciones, el servidor retendría todos los mensajes emitidos por los comandos echo y los entregaría en un único bloque al finalizar el bucle, perdiendo el comportamiento en tiempo real.
Estructura del Backend y Control de Conexión
Al implementar la respuesta en streaming (por ejemplo, utilizando response()->stream() en Laravel), se debe verificar constantemente el estado de la conexión para liberar recursos en caso de que el cliente cierre la pestaña o pierda la señal.
El siguiente flujo ejemplifica la estructura de emisión de eventos:
- Verificación de desconexión: Se evalúa la función
connection_aborted()antes de cada iteración. Si el cliente se ha desconectado, se interrumpe la ejecución del bucle. - Formato de mensaje SSE: La especificación SSE requiere la estructura básica
event: nombre\ndata: { ... }\n\n. - Notificación de cierre: Al finalizar el proceso pesado o el bucle de datos, se envía un evento personalizado (por ejemplo,
event: close) para avisar al cliente que la transmisión ha terminado. - Cabeceras HTTP requeridas: La respuesta debe incluir las cabeceras
Content-Type: text/event-stream,Cache-Control: no-cacheyConnection: keep-alive.
Recepción del Flujo en el Cliente (JavaScript / Vue)
En el navegador no se requieren librerías adicionales, ya que se utiliza la API nativa EventSource para consumir el endpoint:
- Instanciación: Se crea una instancia de
EventSource('/api/v1/events')apuntando a la ruta de la API. - Escucha de eventos personalizados: Se registran escuchadores mediante
addEventListener('message', ...)para procesar la información entrante y agregarla a las estructuras de datos reactivas de la interfaz. - Gestión de cierre y errores: Se escucha el evento de cierre configurado por el servidor o el evento de error (
onerror) para invocar la función de desconexión (eventSource.close()) y restablecer el estado de la interfaz.
1. ¿Qué es Server-Sent Events?
Server-Sent Events (SSE) es una tecnología que permite al servidor de Laravel enviar actualizaciones en tiempo real a la aplicación Vue a través de una sola conexión HTTP abierta.
Ventajas de SSE
- • Funciona sobre HTTP estándar
- • No requiere servidor extra (Pusher, Reverb…)
- • Reconexión automática integrada en el navegador
- • Parseo automático del formato de datos
- • Compatible con todos los navegadores modernos
Limitaciones
- • Unidireccional (solo servidor → cliente)
- • Máximo 6 conexiones por dominio (HTTP/1.1)
- • No ideal para alta frecuencia (<1s entre eventos)
- • Blocking de worker PHP durante el stream
SSE vs WebSockets
| Característica | SSE | WebSockets |
|---|---|---|
| Dirección | Unidireccional | Bidireccional |
| Protocolo | HTTP/1.1 o HTTP/2 | ws:// o wss:// |
| Reconexión | Automática (EventSource) | Manual (implementar) |
| Infraestructura | HTTP estándar | Pusher, Reverb, Socket.io… |
| Complejidad | Baja | Media-Alta |
2. Arquitectura del flujo
┌────────────┐ GET /api/v1/events ┌────────────────┐
│ │ ───────────────────────────────▶ │ │
│ │ │ Laravel │
│ Vue 3 │ HTTP 200 │ (PHP Worker) │
│ Browser │ ◀── Content-Type: text/event-... │ │
│ │ │ │
│ │ data: {"msg":"Notif #1"}\n\n │ sleep(2) × 10 │
│ │ data: {"msg":"Notif #2"}\n\n │ │
│ EventSource data: {"msg":"Notif #3"}\n\n │ → event: │
│ .onmessage ... │ closed │
│ │ event: closed\n\n │ │
│ │ ──── connection.close() ──────▶ │ flush() │
└────────────┘ └────────────────┘- El cliente (Vue) abre una conexión HTTP normal hacia
/api/v1/events. - Laravel no cierra la respuesta: establece la cabecera
Content-Type: text/event-stream. - En un bucle, envía datos formateados como
data: {JSON}\n\n. - El
EventSourcedel navegador recibe cada evento y ejecutaonmessage. - Al terminar, el servidor envía un evento
event: closedy cierra la conexión. - El cliente llama
EventSource.close()para limpiar el socket.
3. Backend — Controlador Laravel
Archivo: app/Http/Controllers/Api/SSEController.php
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\StreamedResponse;
class SSEController extends Controller
{
public function stream(): StreamedResponse
{
return response()->stream(function () {
// Desactivar el buffering de PHP para enviar datos al instante
if (ob_get_level() > 0) {
ob_end_flush();
}
flush();
// Simulamos envío de 10 notificaciones
for ($i = 1; $i <= 10; $i++) {
if (connection_aborted()) {
break;
}
echo $this->formatEvent($i);
flush();
sleep(2);
}
// Avisamos al cliente para que cierre la conexión limpiamente
echo $this->formatClosedEvent();
flush();
}, 200, $this->sseHeaders());
}
/**
* Formato obligatorio de SSE: data: {JSON}\n\n
*/
public function formatEvent(int $iteration): string
{
$data = json_encode([
'message' => "Notificación #{$iteration}",
'time' => now()->toTimeString(),
], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
return "data: {$data}\n\n";
}
/**
* Evento final para cerrar la conexión sin reconexión automática.
*/
public function formatClosedEvent(): string
{
return "event: closed\ndata: {\"message\":\"Stream finalizado\"}\n\n";
}
/**
* Cabeceras SSE: no-cache, keep-alive, Nginx buffering off.
*/
public function sseHeaders(): array
{
return [
'Cache-Control' => 'no-cache',
'Content-Type' => 'text/event-stream; charset=utf-8',
'Connection' => 'keep-alive',
'X-Accel-Buffering' => 'no',
];
}
}Puntos clave del controlador
1
StreamedResponse en vez de un JSON normal. El callback se ejecuta línea por línea, sin cerrar el socket HTTP.2
echo llegue al navegador inmediatamente, no al final del script.3
4
5
4. Backend — Ruta
Archivo: routes/api.php
<?php
use App\Http\Controllers\Api\SSEController;
// Server-Sent Events: flujo unidireccional de notificaciones en tiempo real
Route::get('/v1/events', [SSEController::class, 'stream']);Laravel prefixa automáticamente /api a todas las rutas en routes/api.php, por lo que la URL final es GET /api/v1/events. Se puede comprobar con php artisan route:list --path=events.
Nota sobre conexiones
La ruta no usa middleware auth:sanctum para poder probarla fácilmente con un navegador. En producción, añadir autenticación para proteger el endpoint. El EventSource del navegador envía cookies de sesión automáticamente si la ruta está en el mismo dominio.
5. Frontend — Componente Vue 3
Archivo: resources/js/vue/componets/SSEComponent.vue
<template>
<div>
<div>
<h1>
Notificaciones en vivo (SSE)
</h1>
<div>
<o-button
v-if="!isConnected"
iconLeft="play"
variant="success"
@click="connect"
>Conectar</o-button>
<o-button
v-else
iconLeft="stop"
variant="danger"
@click="disconnect"
>Desconectar</o-button>
</div>
<p v-if="isConnected">
Conectado al stream...
</p>
<ul v-if="messages.length > 0">
<li
v-for="(msg, index) in messages"
:key="index"
>
<strong>[{{ msg.time }}]:</strong> {{ msg.message }}
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
data() {
return {
messages: [],
eventSource: null,
isConnected: false,
}
},
beforeUnmount() {
this.disconnect()
},
methods: {
connect() {
// EventSource gestiona la conexión y reconexión automática
this.eventSource = new EventSource('/api/v1/events')
this.eventSource.addEventListener('open', () => {
this.isConnected = true
})
// Se ejecuta por cada data: {JSON}\n\n
this.eventSource.addEventListener('message', (event) => {
this.messages.push(JSON.parse(event.data))
})
// Escuchar evento custom "closed" del servidor
this.eventSource.addEventListener('closed', () => {
this.disconnect()
})
this.eventSource.addEventListener('error', () => {
if (this.eventSource.readyState === 2) {
this.isConnected = false
}
})
},
disconnect() {
if (this.eventSource) {
this.eventSource.close()
this.eventSource = null
this.isConnected = false
}
},
},
}
</script>Puntos clave del componente
1
data: ... automáticamente.2
EventSource reintenta la conexión solo. Al recibir event: closed, se cierra manualmente antes.3
4
EventSource.CLOSED. Solo actualizamos isConnected cuando la conexión realmente se cerró (2), no cuando está reconectando (0).6. Router y navegación
Ruta agregada en router.js
import SSE from "./componets/SSEComponent.vue";
// Agregado a la lista de rutas:
{
name: 'sse',
path: '/vue/sse',
component: SSE
}Enlace en App.vue
<router-link
:to="{ name: 'sse' }"
>
SSE
</router-link>El enlace está disponible para todos los usuarios (sin condición v-if="$root.isLoggedIn").
7. Tests (Pest PHP)
Archivo: tests/Feature/SSEControllerTest.php
<?php
use App\Http\Controllers\Api\SSEController;
describe('SSEController', function () {
describe('stream', function () {
it('returns an SSE response with the correct headers', function () {
$response = $this->get('/api/v1/events');
$response->assertOk();
expect($response->headers->get('Cache-Control'))
->toContain('no-cache');
$response->assertHeader(
'Content-Type',
'text/event-stream; charset=utf-8'
);
$response->assertHeader('Connection', 'keep-alive');
$response->assertHeader('X-Accel-Buffering', 'no');
});
it('streams events in the SSE format', function () {
$controller = new SSEController;
$event = $controller->formatEvent(1);
expect($event)
->toMatch('/^data: \{"message":"Notificación #1",/')
->toEndWith("\n\n");
});
it('encodes JSON without escaping accents', function () {
$controller = new SSEController;
$json = trim(substr(
$controller->formatEvent(2),
strlen('data: ')
));
expect(json_decode($json, true))
->toMatchArray([
'message' => 'Notificación #2',
]);
});
it('formats the closing event with the closed name', function () {
$controller = new SSEController;
expect($controller->formatClosedEvent())
->toStartWith('event: closed')
->toContain('"message":"Stream finalizado"');
});
});
});Ejecutar los tests:
php artisan test --compact --filter=SSEControllerTest
8. Verificación con curl
Para comprobar el stream desde la terminal:
# Ver los primeros eventos (~6 segundos)
curl -sN --max-time 6 http://larafirststep.test/api/v1/events
# Salida esperada:
data: {"message":"Notificación #1","time":"13:18:42"}
data: {"message":"Notificación #2","time":"13:18:44"}
# Ver los 10 eventos completos + el cierre (~24 segundos)
curl -sN --max-time 24 http://larafirststep.test/api/v1/events | tail -3
# Últimas líneas:
event: closed
data: {"message":"Stream finalizado"}El flag -N
El flag -N (equivalente a --no-buffer) le dice a curl que muestre cada línea a medida que llega, sin esperar al buffer completo del servidor.
9. Notas para producción
Worker blocking
Durante el stream, el worker PHP queda bloqueado (ocupado) durante todo el ciclo (sleep(2) × 10 = 20s). En producción con pocos workers (FPM), esto puede saturar el servidor.
Soluciones:
- • Usar Swoole o Octane (servidor async)
- • Publicar eventos vía Redis Pub/Sub con
Redis::subscribe() - • Usar Queues para escribir a un archivo/Redis y el stream lo lee en tiempo real
Rate limiting
Las conexiones SSE duran mucho más que una petición normal. Añadir throttle o limitar por IP/usuario para evitar abusos.
Autenticación
El EventSource envía cookies automáticamente si la ruta está en el mismo dominio. Para APIs externas, usar tokens en la URL o en un header personalizado (requiere configurar withCredentials: true).
Escalabilidad
Si necesitas broadcasting a miles de clientes simultáneos, considera Laravel Reverb con WebSockets o servicios como Pusher. SSE es ideal para dashboards internos, notificaciones de admin o demos donde el volumen concurrente es moderado.
10. Archivos creados o modificados
M resources/js/vue/App.vue ← enlace "SSE" en el nav
M resources/js/vue/router.js ← ruta /vue/sse
M routes/api.php ← GET /api/v1/events
?? app/Http/Controllers/Api/SSEController.php ← controlador SSE
?? resources/js/vue/componets/SSEComponent.vue ← componente Vue
?? tests/Feature/SSEControllerTest.php ← tests Pest (4)Pasos para probar
- Asegurarse de que Vite esté corriendo:
npm run dev - Abrir la app en el navegador:
http://larafirststep.test/vue/sse - Hacer clic en Conectar
- Ver las notificaciones llegando cada 2 segundos
- Al finalizar (evento
closed), la conexión se cierra automáticamente