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.
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
| SSE | WebSockets | |
|---|---|---|
| Direction | Server → client only | Both directions |
| Protocol | Plain HTTP | Custom (after upgrade) |
| Auto-reconnect | Yes, built in | You write it |
| Binary support | No (text only) | Yes |
| Browser support | Universal | Universal |
For dashboards, progress bars, notifications, or chat receive feeds, SSE is plenty. Reach for WebSockets when you need true two-way comms.
Config Module →