ADR-0001. Horizontally-scaled WebSocket via a single Redis pub/sub bus
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-03-28 |
| Deciders | signal-team |
| Supersedes | — |
Context
- Real-time updates (orders, payments) must reach POS/Admin clients regardless of which Signal instance holds the socket.
- WebSocket connections are sticky to one process; in-memory client state cannot be shared across replicas.
- Other services (Sale, Payment) must be able to push to clients without hosting their own WebSocket server.
- We want a single real-time edge, not a WebSocket server embedded in every service.
Decision
We will run Signal as the only WebSocket server and route all cross-instance / cross-service delivery through a single Redis pub/sub bus.
- Signal hosts
WebSocketServerHelper(subscribe + publish); producers hold a lightweightWebSocketEmitter(publish only). sendToClientdelivers locally if the client is on this instance, otherwise re-publishes to Redis for the owning instance.broadcast/sendToRoomalways publish to Redis so every instance delivers to its local matching clients.- Redis runs in
singleorclustermode (APP_ENV_WEBSOCKET_REDIS_*); all participants must share the same instance.
Consequences
| Pros | Cons |
|---|---|
| Stateless replicas — scale horizontally behind Traefik | Redis becomes a hard dependency / single point of failure for fan-out |
| Producers stay thin (emitter only, no WS server) | Cross-instance delivery is best-effort (dropped if no instance subscribed) |
| One real-time edge to secure and operate | All publishers must be configured against the identical Redis |
Alternatives Considered
| Option | Pros | Cons | Why rejected |
|---|---|---|---|
| WS server embedded in each service | No shared bus | N servers to secure; clients juggle N sockets | Operationally heavy, duplicates auth/encryption |
| Sticky sessions, no bus | Simple | Breaks broadcast + cross-instance delivery | Doesn't meet "reach any client" requirement |
| Kafka as the delivery bus | Already present | Higher latency, not built for ephemeral pub/sub fan-out | Redis pub/sub fits live delivery better |
References
signal/src/components/websocket.component.ts(NxWebSocketComponent,bindEmitter)signal/src/services/signal-event.service.ts(sendToClientlocal/remote routing)- Operations — multi-instance fan-out