Server-Sent Events

Server-Sent Events

Server-Sent Events (SSE) is a server-push technology built on top of HTTP that allows a server to send a unidirectional stream of events to a client over a long-lived HTTP connection. The browser's native EventSource API manages the connection, including automatic reconnection.

Core Characteristics

  • Direction: server to client only (unidirectional)
  • Protocol: standard HTTP/1.1 or HTTP/2 — no protocol upgrade required
  • Format: text/event-stream MIME type; each event is a block of field: value lines separated by blank lines
  • Reconnection: automatic — the browser EventSource reconnects after connection loss using the Last-Event-ID header to resume from the last received event
  • Authentication: works naturally with session cookies (withCredentials: true) — aligned with the BFF-For-SPA pattern

SSE vs WebSocket

AspectSSEWebSocket
DirectionServer → Client onlyBidirectional
ProtocolHTTP/1.1 or HTTP/2HTTP upgrade to ws:// or wss://
Spring supportFlux<ServerSentEvent<T>>Spring WebSocket + STOMP broker
Angular supportEventSource APIWebSocket API or RxJS wrapper
BFF complexityLow — expose a reactive Flux endpointHigh — BFF must proxy WS or implement STOMP broker relay
Use casesNotifications, live feeds, progress barsChat, collaborative editing, multiplayer
ReconnectionAutomatic (browser-managed)Manual (application must implement)
Cookie authWorks — same HTTP connectionMore complex with session cookies
Firewall traversalUsually fine — standard HTTPSometimes blocked by enterprise proxies
HTTP/2 multiplexingYes — multiple SSE streams over one connectionOne connection per stream

SSE is the right choice for a BFF-Pattern serving an Angular SPA when the data flow is server-initiated: notifications, background job progress, live metric dashboards, and feed updates.

Spring Boot Implementation

Spring WebFlux (via Spring-Cloud-Gateway or a standalone WebFlux controller) exposes SSE endpoints by returning Flux<ServerSentEvent<T>> with produces = MediaType.TEXT_EVENT_STREAM_VALUE.

@GetMapping(
    value = "/api/notifications/stream",
    produces = MediaType.TEXT_EVENT_STREAM_VALUE
)
public Flux<ServerSentEvent<NotificationDto>> streamNotifications(
        @AuthenticationPrincipal Mono<OidcUser> user) {
    return user.flatMapMany(u ->
        notificationService.getNotificationsForUser(u.getSubject())
            .map(notification -> ServerSentEvent.<NotificationDto>builder()
                .id(notification.id())
                .event("notification")
                .data(notification)
                .build())
    );
}

Key points:

  • @AuthenticationPrincipal Mono<OidcUser> — reactive security principal injection; works because the BFF session cookie carries the authenticated principal
  • flatMapMany — converts the Mono<OidcUser> to a Flux of events for that user
  • ServerSentEvent.builder() — sets the SSE id:, event:, and data: fields
  • The response Content-Type: text/event-stream keeps the HTTP connection open

Heartbeat to Prevent Proxy Timeouts

Long-lived SSE connections are often closed by intermediate proxies after 30–60 s of silence. A periodic comment keeps the connection alive:

Flux<ServerSentEvent<NotificationDto>> heartbeat = Flux
    .interval(Duration.ofSeconds(25))
    .map(tick -> ServerSentEvent.<NotificationDto>builder()
        .comment("heartbeat")
        .build());
 
return Flux.merge(eventStream, heartbeat);

Angular Client Implementation

Angular consumes SSE via the native browser EventSource API. The key is withCredentials: true so the session cookie is sent.

import { Injectable, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
 
export interface NotificationDto {
  id: string;
  message: string;
  type: 'INFO' | 'WARNING' | 'ERROR';
}
 
@Injectable({ providedIn: 'root' })
export class NotificationStreamService implements OnDestroy {
  private eventSource: EventSource | null = null;
  private notificationSubject = new Subject<NotificationDto>();
 
  readonly notifications$ = this.notificationSubject.asObservable();
 
  connect(): void {
    if (this.eventSource) return;
 
    this.eventSource = new EventSource('/api/notifications/stream', {
      withCredentials: true  // send session cookie
    });
 
    this.eventSource.addEventListener('notification', (event: MessageEvent) => {
      const notification: NotificationDto = JSON.parse(event.data);
      this.notificationSubject.next(notification);
    });
 
    this.eventSource.onerror = () => {
      // EventSource reconnects automatically — log for diagnostics only
      console.warn('SSE connection error, browser will reconnect');
    };
  }
 
  disconnect(): void {
    this.eventSource?.close();
    this.eventSource = null;
  }
 
  ngOnDestroy(): void {
    this.disconnect();
  }
}

Wrapping in RxJS

For idiomatic Angular usage, wrap EventSource in an Observable:

import { Observable } from 'rxjs';
 
function fromEventSource<T>(url: string, eventName: string): Observable<T> {
  return new Observable(observer => {
    const es = new EventSource(url, { withCredentials: true });
    es.addEventListener(eventName, (e: MessageEvent) => {
      observer.next(JSON.parse(e.data) as T);
    });
    es.onerror = (err) => observer.error(err);
    return () => es.close();
  });
}

BFF Advantages for SSE

The BFF-Pattern is particularly well-suited for SSE:

  1. Authentication: the BFF already manages the session cookie — the SSE endpoint inherits the authenticated session with no extra work
  2. Fan-out multiplexing: the BFF can merge events from multiple upstream sources into a single SSE stream for the Angular client
  3. Transformation: events from disparate downstream schemas are normalised into the Angular-facing NotificationDto before streaming
  4. Security boundary: downstream services emit raw events over internal channels (Kafka, Redis Pub/Sub); the BFF decides what to expose and to which users

Relationship to Reactive Stack

SSE is a natural fit for Project-Reactor — a Flux is literally an asynchronous stream that maps 1:1 onto the SSE wire format. A blocking servlet stack (Spring MVC) can technically serve SSE but holds a thread per connection; Reactive-Programming (WebFlux) handles thousands of concurrent SSE streams with a small thread pool.