Conexiones Real Time: Server Sent Events

Los Server Sent Events (SSE) son una tecnología basada en HTTP que permite al servidor enviar datos al cliente de forma unidireccional y en tiempo real. A diferencia del modelo tradicional request/response, donde el cliente debe preguntar constantemente por nuevos datos (polling), con SSE el servidor mantiene la conexión abierta y envía información automáticamente cuando está disponible.

SSE está soportado de forma nativa por los navegadores modernos mediante la API EventSource, lo que lo convierte en una solución sencilla y eficiente para:

  • Notificaciones en tiempo real
  • Actualización de dashboards
  • Logs en streaming
  • Sistemas de monitorización
  • Chats unidireccionales

La comunicación se realiza sobre HTTP estándar con el tipo de contenido:

text/event-stream

Cada evento sigue un formato simple:

event: nombreEvento
data: contenido
id: 1

Alternativas: WebSockets

La alternativa más conocida a SSE es WebSockets, una tecnología que permite comunicación bidireccional full-duplex entre cliente y servidor sobre una única conexión TCP persistente.

Diferencias conceptuales

CaracterísticaSSEWebSockets
DirecciónUnidireccional (Servidor → Cliente)Bidireccional
ProtocoloHTTP estándarws / wss
Reconexión automáticaSí (nativa)No (manual)
ComplejidadBajaMedia/Alta

Tabla comparativa: Ventajas y Desventajas

TecnologíaVentajasDesventajas
SSESimple de implementar. Basado en HTTP. Reconexión automática. Ideal para notificacionesSolo unidireccional. No soporta binarios nativamente. Limitado en algunos proxies antiguos
WebSocketsComunicación bidireccional. Mayor flexibilidad. Soporte binarioMayor complejidad. Gestión manual de reconexión. No siempre compatible con infraestructuras legacy.

Implementación clásica con Spring Boot 3

En Spring Boot 3, la forma tradicional de implementar SSE es mediante SseEmitter

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
/** Service **/
public class SSEService {
        
	private Map<String, SseEmitter> emitters = new ConcurrentHashMap<>();
	public SseEmitter subscribe(String userId) {
        SseEmitter emitter = emitters
            .computeIfAbsent(userId, k -> new SseEmitter(0L));

        emitter.onCompletion(() -> {
        	log.info("Connection COMPLETED on USER: "+ userId);
        	remove(userId);});
        emitter.onTimeout(() -> {
        	log.info("Connection TIMEOUT on USER: "+ userId);
        	remove(userId);});
        emitter.onError(e -> {
        	log.info("Connection ERROR on USER: "+ userId);
        	remove(userId);});
        
        objs.computeIfAbsent(userId, k -> obj);

        return emitter;
    }
    public void remove(String userId) {
    	SseEmitter sse = emitters.get(userId);
        if (sse != null) {
        	log.info("CLEAN CONNECTION AND REMOVE FILTER on USER: "+ userId);
        	emitters.remove(userId);
        }
    }
}
/* Controller */
public class Controller {
        
	@Autowired
	private SSEService sseService;
	@GetMapping(path = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
	public SseEmitter subscribe(@RequestParam("userID") String userId) {
		log.debug("Call to SSE Subscribe");
		return service.subscribe(userId);
	}
}

Características clave

  • Spring mantiene la conexión abierta
  • El navegador reconecta automáticamente si se pierde
  • Se puede personalizar timeout y gestión de errores

Avanzado

Programación Reactiva

En aplicaciones modernas, especialmente con alta concurrencia, el modelo bloqueante tradicional no es óptimo. Aquí entra la programación reactiva, basada en:

  • Streams asíncronos
  • Backpressure
  • No bloqueo de hilos

Spring ofrece soporte mediante Project Reactor, usando tipos como:

  • Flux<T>
  • Mono<T>

Para SSE, Flux es perfecto porque representa una secuencia de datos potencialmente infinita.


Uso con Spring WebFlux

Para entornos altamente concurrentes o arquitecturas basadas en microservicios, WebFlux es la opción recomendada.

Dependencia

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

Implementación con Flux

/* Service */
@Service
public class SseService {

    private final Map<String, Sinks.Many<String>> sinks = new ConcurrentHashMap<>();

    public Sinks.Many<String> createSink(String clientId) {
        return sinks.computeIfAbsent(clientId,
                id -> Sinks.many().multicast().onBackpressureBuffer());
    }

    public void publish(String clientId, String message) {
        Sinks.Many<String> sink = sinks.get(clientId);
        if (sink != null) {
            sink.tryEmitNext(message);
        }
    }

    public Flux<ServerSentEvent<String>> subscribe(String clientId) {
        Sinks.Many<String> sink = createSink(clientId);

        Flux<ServerSentEvent<String>> flux = sink.asFlux()
                .map(data -> ServerSentEvent.builder(data).build());

        // heartbeat to keep connections alive
        Flux<ServerSentEvent<String>> heartbeat = Flux.interval(Duration.ofSeconds(15))
                .map(i -> ServerSentEvent.<String>builder().comment("heartbeat").build());

        return Flux.merge(flux, heartbeat)
                .doOnCancel(() -> sinks.remove(clientId));
    }
}
/* CONTROLLER */
@RestController
@RequestMapping("/sse")
public class SseController {
    @Autowire
    SseService sseService;

    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<String>> stream(@QueryParam String userId) {
        return sseService.subscribe(userId)
    }
}

Esto es lo que sucede:

  1. Cada cliente obtiene su propio receptor (un publicador reactivo).
  2. Cuando el servidor llama a publish(clientId, message), ese mensaje se envía a todos los suscriptores.
  3. Los latidos garantizan que los proxies no interrumpan la conexión.
  4. Cuando un cliente se desconecta, el receptor se limpia.

Ventajas del enfoque reactivo

  • No bloquea hilos
  • Escala mejor con muchas conexiones abiertas
  • Integración natural con bases de datos reactivas
  • Soporte de backpressure

Conclusión

Los Server-Sent Events son una solución elegante y eficiente para escenarios donde el servidor necesita enviar información en tiempo real al cliente sin requerir comunicación bidireccional.

  • Si necesitas simplicidad → SSE clásico con Spring MVC
  • Si necesitas alta concurrencia y escalabilidad → SSE con WebFlux
  • Si necesitas bidireccionalidad → WebSockets

En arquitecturas modernas basadas en microservicios y sistemas event-driven, SSE sigue siendo una herramienta extremadamente útil, especialmente cuando se combina con programación reactiva.

A lo mejor te interesa

What is HTTP Streaming and How Does it Work?

stomp-js/stompjs: Javascript and Typescript Stomp client for Web browsers and node.js apps

WebSockets – API web | MDN

Stack para Testing (Swarm + jMeter)

Real-Time Applications with Server-Sent Events (SSE) in Spring Boot | by Ishara Madusanka | Medium