Skip to content

Server-Sent Events

Nest’s @Sse() decorator works out of the box. The adapter bridges Nest’s Node-stream SSE machinery onto a Bun-native ReadableStream, so events are streamed over Web Streams with no Node http server underneath.

A handler decorated with @Sse() returns an Observable<MessageEvent>. Each emitted value is serialized into the W3C text/event-stream format.

import { Controller, Sse } from '@nestjs/common';
import { interval, map, type Observable } from 'rxjs';
@Controller('events')
export class EventsController {
@Sse('clock')
clock(): Observable<{ data: { now: string } }> {
return interval(1000).pipe(
map(() => ({ data: { now: new Date().toISOString() } })),
);
}
}

Consume it from the browser:

const source = new EventSource('/events/clock');
source.onmessage = (e) => console.log(JSON.parse(e.data));

Each value follows Nest’s MessageEvent shape:

FieldEffect on the wire
dataThe payload. Objects are JSON-stringified; strings are sent verbatim.
typeEmitted as event: <type>, so addEventListener('<type>', …) fires.
idEmitted as id: <id> (auto-incremented when omitted).
retryReconnection hint in milliseconds.
@Sse('notifications')
notifications(): Observable<{ type: string; data: unknown }> {
return this.notifications$.pipe(
map((n) => ({ type: 'notification', data: n })),
);
}

The adapter applies real backpressure: when the client reads slower than the Observable emits, writes pause until the socket drains, so a fast producer can’t buffer without bound.

When the client disconnects, the request’s AbortSignal fires, Nest unsubscribes from your Observable, and any teardown logic runs:

@Sse('feed')
feed(): Observable<{ data: unknown }> {
return new Observable((subscriber) => {
const handle = subscribe(subscriber);
return () => handle.unsubscribe(); // runs on client disconnect
});
}