Skip to content

Pick one mechanism per flow — don't confuse SSE with WebSocket

Use SSE for one-way LLM token streaming — reserve WebSocket for bidirectional chat.

nestjs-realtime-transport-sse-vs-websocket

Why it matters

Failure modes if this rule is ignored
StakeIf ignored
Can't scale
  • WebSocket is operationally more expensive (sticky sessions, scaling, connections that don't close).
  • Proxies and load balancers treat long-lived sockets differently from plain HTTP/SSE.

How to fix

  • SSE (Server-Sent Events) — one-way stream from server to client. Ideal for LLM token streaming, job progress, notifications.
  • WebSocket — bidirectional. Required only when the client sends frequent unsolicited messages (collaborative editing, user-to-user chat).
  • Long polling — fallback, not default.

Examples

Bad
ts
@WebSocketGateway()
export class ChatGateway {
  @SubscribeMessage('prompt')
  async handlePrompt(@MessageBody() data: any, @ConnectedSocket() client: Socket) {
    for await (const token of this.llm.stream(data.prompt)) {
      client.emit('token', token);
    }
  }
}
Good
ts
@Controller('chat')
export class ChatController {
  @Post('stream')
  @Sse()
  async stream(@Body() dto: PromptDto): Promise<Observable<MessageEvent>> {
    return new Observable((subscriber) => {
      (async () => {
        for await (const token of this.llm.stream(dto.prompt)) {
          subscriber.next({ data: { token } });
        }
        subscriber.complete();
      })();
    });
  }
}

Contribute

Released under the MIT License.

esc