Server-Sent Events

Push Updates Over Plain HTTP

Server-Sent Events

SSE is the simpler alternative to WebSockets when you only need server-to-client streaming. Plain HTTP, auto-reconnect, no extra deps.

4 min read Level 2/5 #nestjs#sse#realtime
What you'll learn
  • Define an SSE endpoint with @Sse
  • Return an Observable of MessageEvent
  • Recognize the right use cases (notifications, progress)

WebSockets are great when both sides talk back and forth. But a lot of real-time use cases are one-way: the server has something to push, the client just wants to listen. Server-Sent Events is built for that — plain HTTP, no upgrades, no extra dependencies.

The Endpoint

@Sse() is the SSE decorator. Return an RxJS Observable<MessageEvent> and Nest streams each emission to the client.

import { Controller, Sse, MessageEvent } from '@nestjs/common';
import { Observable, interval, map } from 'rxjs';

@Controller('notifications')
export class NotificationsController {
  @Sse('stream')
  stream(): Observable<MessageEvent> {
    return interval(1000).pipe(
      map((n) => ({ data: { count: n } } as MessageEvent)),
    );
  }
}

Hit /notifications/stream from a browser with EventSource and you’ll receive an event every second. No reconnection logic to write — the browser handles it automatically.

On the Client

const source = new EventSource('/notifications/stream');

source.onmessage = (e) => {
  const payload = JSON.parse(e.data);
  console.log('tick', payload.count);
};

source.onerror = () => {
  // browser auto-reconnects; this is just for logging
};

EventSource is built into every modern browser. No extra library, no handshake, no auth dance — just an HTTP GET that stays open.

A Real Example: Per-User Notifications

Combine an in-memory Subject per user with the @Sse handler and you have a basic notification bus.

import { Injectable } from '@nestjs/common';
import { Subject, Observable } from 'rxjs';

@Injectable()
export class NotificationsService {
  private streams = new Map<number, Subject<MessageEvent>>();

  for(userId: number): Observable<MessageEvent> {
    if (!this.streams.has(userId)) {
      this.streams.set(userId, new Subject());
    }
    return this.streams.get(userId)!.asObservable();
  }

  push(userId: number, event: MessageEvent) {
    this.streams.get(userId)?.next(event);
  }
}

@Controller('notifications')
export class NotificationsController {
  constructor(private readonly notifications: NotificationsService) {}

  @UseGuards(JwtAuthGuard)
  @Sse('me')
  me(@Request() req): Observable<MessageEvent> {
    return this.notifications.for(req.user.id);
  }
}

For more than a single instance, swap the in-memory Subject for a Redis pub/sub channel — same API, distributed delivery.

SSE vs WebSockets

SSEWebSockets
DirectionServer → client onlyBoth directions
ProtocolPlain HTTPCustom (after upgrade)
Auto-reconnectYes, built inYou write it
Binary supportNo (text only)Yes
Browser supportUniversalUniversal

For dashboards, progress bars, notifications, or chat receive feeds, SSE is plenty. Reach for WebSockets when you need true two-way comms.

Config Module →