Angular 120 🅰️ Angular and WebSockets
HTTP is a request-response protocol. The client asks, the server answers, and the connection closes. WebSockets break that model: a single TCP connection stays open, and both the client and the server can send messages at any time. This makes WebSockets the right tool for applications where the server must push data without waiting for a request — chat, live notifications, collaborative editing, real-time dashboards, multiplayer games, and financial tickers.
Angular does not ship with a WebSocket service. The browser provides the WebSocket API natively, and Angular applications wrap it in a service that exposes an RxJS observable. The observable receives messages as they arrive, and components subscribe to it with the AsyncPipe. The wrapper handles connection lifecycle, reconnection, error handling, and cleanup so that components do not need to manage the raw socket.
This chapter covers the WebSocket protocol, the browser’s WebSocket API, wrapping the socket in an Angular service with RxJS, reconnection strategies with retry and retryWhen, sending messages, cleaning up on component destruction, and the patterns for real-time state in Angular applications.
Key point: The browser’s WebSocket API is callback-based. Wrap it in a service that returns an RxJS observable so components can subscribe with the AsyncPipe. Handle reconnection, error, and completion explicitly. Use takeUntilDestroyed or unsubscribe on component destruction to prevent leaks.
Why WebSockets exist alongside HTTP
The polling problem. Before WebSockets, real-time behavior was simulated with polling: the client sends an HTTP request every few seconds to check for new data. Polling wastes bandwidth when nothing has changed, introduces latency equal to the polling interval, and scales poorly because every client generates a request on every interval. Long polling improves on this by holding the request open until the server has data, but each message still requires a new request.
The server-push problem. Some applications need the server to initiate communication. A chat application must deliver a message the moment it is sent. A stock ticker must update the price the moment it changes. HTTP is inherently client-initiated, so the server cannot push without a persistent connection.
The bidirectional problem. Some applications need both sides to send messages continuously. A collaborative editor sends edits as the user types and receives edits from other users simultaneously. HTTP cannot do this in a single connection.
The overhead problem. Every HTTP request carries headers — cookies, user agent, content type, and so on — that are redundant after the first request. WebSockets perform a handshake once and then send raw frames with minimal overhead, which matters for high-frequency messages.
The latency problem. Polling and long polling add latency between the moment data is available and the moment the client receives it. WebSockets deliver messages immediately, which is essential for interactive applications.
a. The WebSocket protocol
A WebSocket connection begins as an HTTP request with an Upgrade header. The server responds with 101 Switching Protocols, and the connection becomes a full-duplex WebSocket. From that point on, both sides send frames — text or binary — over the same TCP connection.
The handshake includes a Sec-WebSocket-Key from the client and a Sec-WebSocket-Accept from the server, derived from the key. This verifies that the server understood the WebSocket protocol and is not an accidental HTTP response.
WebSocket URLs use the ws:// scheme for unencrypted connections and wss:// for encrypted connections. In production, wss:// is mandatory because anything on the network path can read an unencrypted WebSocket.
WebSockets are not the only option for server push. Server-Sent Events (SSE) provide one-way server-to-client streaming over HTTP and are simpler when the client does not need to send messages. WebSockets are bidirectional and lower-overhead, but they require a more complex server and do not reconnect automatically.
b. The browser WebSocket API
The browser exposes WebSocket as a global class. It has four properties and four methods that matter for application code.
| Member | Type | Purpose |
|---|---|---|
readyState | number | Connection state: 0–3 |
send(data) | method | Send a message |
close(code, reason) | method | Close the connection |
onopen | event | Connection established |
onmessage | event | Message received |
onerror | event | Error occurred |
onclose | event | Connection closed |
const socket = new WebSocket('wss://example.com/socket');
socket.onopen = () => console.log('connected');
socket.onmessage = (event) => console.log('message:', event.data);
socket.onerror = (event) => console.error('error:', event);
socket.onclose = (event) => console.log('closed', event.code, event.reason);
socket.send(JSON.stringify({ type: 'subscribe', channel: 'prices' }));
The readyState values are CONNECTING (0), OPEN (1), CLOSING (2), and CLOSED (3). Sending a message when the socket is not OPEN throws an error. The onclose event includes a code and reason that indicate why the connection closed.
The API is callback-based, which means it does not compose with RxJS or Angular’s change detection naturally. Wrapping it in a service is the standard approach.
c. Wrapping WebSocket in an Angular service
A WebSocket service exposes an observable of incoming messages and a method for sending. The service creates the socket when the first subscriber connects and closes it when the last subscriber unsubscribes, or keeps a single connection for the application’s lifetime.
import { Injectable, inject } from '@angular/core';
import { Observable, Subject, timer } from 'rxjs';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
import { retry, share } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class WebSocketService {
private socket$: WebSocketSubject<unknown> | null = null;
connect(url: string): Observable<unknown> {
if (!this.socket$ || this.socket$.closed) {
this.socket$ = webSocket(url);
}
return this.socket$.pipe(
retry({ delay: 5000 }),
share(),
);
}
send(message: unknown): void {
this.socket$?.next(message);
}
close(): void {
this.socket$?.complete();
this.socket$ = null;
}
}
RxJS provides webSocket() as a factory that returns a WebSocketSubject. The subject is both an observer — send pushes messages into it — and an observable that emits incoming messages. The retry({ delay: 5000 }) operator reconnects after five seconds when the connection drops. The share() operator ensures that a single connection is shared among all subscribers.
The service pattern keeps the socket lifecycle out of components. Components inject the service, call connect(url), and subscribe to the returned observable.
d. Consuming the observable in a component
Components consume the service’s observable with the AsyncPipe or an explicit subscription. The AsyncPipe subscribes and unsubscribes automatically, which prevents leaks and integrates with Angular’s change detection.
import { Component, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { WebSocketService } from './websocket.service';
import { map, scan } from 'rxjs';
@Component({
selector: 'app-chat',
imports: [AsyncPipe],
template: `
<ul>
@for (message of messages$ | async; track message.id) {
<li>{{ message.text }}</li>
}
</ul>
`,
})
export class ChatComponent {
private readonly ws = inject(WebSocketService);
messages$ = this.ws.connect('wss://example.com/chat').pipe(
map((raw) => JSON.parse(raw as string)),
scan((acc, msg) => [...acc, msg], [] as Message[]),
);
}
The scan operator accumulates incoming messages into an array, and the AsyncPipe renders the array. When the socket emits a new message, the observable emits a new array, and Angular re-renders the list.
For components that need to react to a message rather than render it — for example, to update state or show a notification — an explicit subscription with takeUntilDestroyed is appropriate:
import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({ /* ... */ })
export class NotificationComponent {
private readonly ws = inject(WebSocketService);
constructor() {
this.ws.connect('wss://example.com/notifications')
.pipe(takeUntilDestroyed())
.subscribe((raw) => {
const notification = JSON.parse(raw as string);
this.showNotification(notification);
});
}
}
takeUntilDestroyed is an Angular 16+ operator that unsubscribes automatically when the component is destroyed, eliminating the need for manual Subscription management and ngOnDestroy.
e. Reconnection and error handling
WebSocket connections drop. Network changes, server restarts, and idle timeouts all close the socket. The client must reconnect, and the reconnection strategy determines the user experience.
The retry operator with a fixed delay reconnects after a fixed interval:
return this.socket$.pipe(
retry({ delay: 5000 }),
share(),
);
For production, exponential backoff is better. It avoids hammering a server that is down and gives the server time to recover:
return this.socket$.pipe(
retry({
delay: (error, retryCount) => {
const delayMs = Math.min(1000 * 2 ** retryCount, 30000);
return timer(delayMs);
},
}),
share(),
);
The delay doubles on each attempt, capped at 30 seconds. After the cap, attempts continue at 30-second intervals.
Errors on the socket are delivered to the observable’s error channel. If retry is not used, the observable errors and completes, and no further messages arrive. Components should handle errors or rely on the service to retry transparently.
The onclose event includes a code that indicates why the connection closed. Codes in the 1000–1015 range are defined by the WebSocket specification, and codes in the 4000–4999 range are application-defined. A code of 1000 means normal closure; 1006 means abnormal closure (the connection dropped without a close frame).
f. Sending messages and connection state
Sending a message requires the socket to be in the OPEN state. The service can expose a method that checks the state and throws or queues if the socket is not ready.
send(message: unknown): boolean {
if (this.socket$ && !this.socket$.closed) {
this.socket$.next(message);
return true;
}
return false;
}
A more robust approach queues messages until the connection is open. The webSocket() factory does not queue automatically; it throws if the socket is not open. The service can maintain a queue and flush it on the onopen event, or use the WebSocketSubject‘s configuration to buffer.
For applications that need connection status in the UI — a green dot for connected, a red dot for disconnected — the service exposes a status$ observable derived from the socket’s onopen and onclose events:
private readonly statusSubject = new BehaviorSubject<'open' | 'closed'>('closed');
readonly status$ = this.statusSubject.asObservable();
The socket’s onopen and onclose handlers call statusSubject.next('open') and statusSubject.next('closed'), and components subscribe to status$ to render the indicator.
Complete Example Session
// ============================================
// PART 1: CREATE A WEBSOCKET SERVICE
// ============================================
import { Injectable } from '@angular/core';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
import { Observable, retry, share } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class WebSocketService {
private socket$: WebSocketSubject<unknown> | null = null;
connect(url: string): Observable<unknown> {
if (!this.socket$ || this.socket$.closed) {
this.socket$ = webSocket(url);
}
return this.socket$.pipe(
retry({ delay: 5000 }),
share(),
);
}
send(message: unknown): void {
this.socket$?.next(message);
}
close(): void {
this.socket$?.complete();
this.socket$ = null;
}
}
// ============================================
// PART 2: CONSUME IN A COMPONENT
// ============================================
import { Component, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { WebSocketService } from './websocket.service';
import { map, scan } from 'rxjs';
@Component({
selector: 'app-chat',
imports: [AsyncPipe],
template: `
<ul>
@for (message of messages$ | async; track message.id) {
<li>{{ message.text }}</li>
}
</ul>
`,
})
export class ChatComponent {
private readonly ws = inject(WebSocketService);
messages$ = this.ws.connect('wss://example.com/chat').pipe(
map((raw) => JSON.parse(raw as string)),
scan((acc, msg) => [...acc, msg], [] as Message[]),
);
}
// ============================================
// PART 3: SEND A MESSAGE
// ============================================
sendMessage(text: string): void {
this.ws.send(JSON.stringify({ type: 'message', text }));
}
// ============================================
// PART 4: EXPONENTIAL BACKOFF RECONNECTION
// ============================================
import { timer } from 'rxjs';
connect(url: string): Observable<unknown> {
if (!this.socket$ || this.socket$.closed) {
this.socket$ = webSocket(url);
}
return this.socket$.pipe(
retry({
delay: (_error, retryCount) => {
const delayMs = Math.min(1000 * 2 ** retryCount, 30000);
return timer(delayMs);
},
}),
share(),
);
}
// ============================================
// PART 5: TAKE UNTIL DESTROYED SUBSCRIPTION
// ============================================
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export class NotificationComponent {
private readonly ws = inject(WebSocketService);
constructor() {
this.ws.connect('wss://example.com/notifications')
.pipe(takeUntilDestroyed())
.subscribe((raw) => {
const notification = JSON.parse(raw as string);
this.showNotification(notification);
});
}
}
// ============================================
// PART 6: TRACK CONNECTION STATUS
// ============================================
import { BehaviorSubject } from 'rxjs';
private readonly statusSubject = new BehaviorSubject<'open' | 'closed'>('closed');
readonly status$ = this.statusSubject.asObservable();
private createSocket(url: string) {
const socket = webSocket({
url,
openObserver: { next: () => this.statusSubject.next('open') },
closeObserver: { next: () => this.statusSubject.next('closed') },
});
return socket;
}
// ============================================
// PART 7: RENDER STATUS INDICATOR
// ============================================
@Component({
selector: 'app-status',
imports: [AsyncPipe],
template: `
<span [class.online]="(status$ | async) === 'open'">
{{ (status$ | async) === 'open' ? 'Connected' : 'Disconnected' }}
</span>
`,
})
export class StatusComponent {
private readonly ws = inject(WebSocketService);
readonly status$ = this.ws.status$;
}
// ============================================
// PART 8: QUEUE MESSAGES UNTIL OPEN
// ============================================
private queue: unknown[] = [];
send(message: unknown): void {
if (this.socket$ && !this.socket$.closed) {
this.socket$.next(message);
} else {
this.queue.push(message);
}
}
private flushQueue(): void {
this.queue.forEach((msg) => this.socket$?.next(msg));
this.queue = [];
}
// ============================================
// PART 9: HANDLE SPECIFIC MESSAGE TYPES
// ============================================
messages$
.pipe(
filter((msg) => msg.type === 'price'),
map((msg) => msg.payload),
)
.subscribe((price) => this.updatePrice(price));
// ============================================
// PART 10: CLEAN UP ON SERVICE DESTRUCTION
// ============================================
import { DestroyRef, inject } from '@angular/core';
export class WebSocketService {
private readonly destroyRef = inject(DestroyRef);
constructor() {
this.destroyRef.onDestroy(() => this.close());
}
}
These ten parts cover the service, component consumption, sending, exponential backoff, takeUntilDestroyed, connection status, status rendering, message queuing, message filtering, and cleanup.
Quick Reference
Browser WebSocket API
| Member | Purpose |
|---|---|
new WebSocket(url) | Create connection |
readyState | 0 = CONNECTING, 1 = OPEN, 2 = CLOSING, 3 = CLOSED |
send(data) | Send a message |
close(code, reason) | Close the connection |
onopen | Connection established |
onmessage | Message received |
onerror | Error occurred |
onclose | Connection closed |
RxJS WebSocket API
| API | Purpose |
|---|---|
webSocket(url) | Create WebSocketSubject |
socket.next(msg) | Send a message |
socket.subscribe(fn) | Receive messages |
socket.complete() | Close the connection |
retry({ delay }) | Reconnect on error |
share() | Share one connection among subscribers |
Close Codes
| Code | Meaning |
|---|---|
| 1000 | Normal closure |
| 1001 | Going away |
| 1006 | Abnormal closure (no close frame) |
| 1011 | Server error |
| 4000–4999 | Application-defined |
Angular Integration
| Pattern | Purpose |
|---|---|
Service with webSocket() | Encapsulate the socket |
AsyncPipe on the observable | Subscribe and unsubscribe automatically |
takeUntilDestroyed() | Unsubscribe on component destroy |
BehaviorSubject for status | Expose connection state |
DestroyRef.onDestroy() | Clean up on service destroy |
Best Practices
✅ Do This:
// Wrap the socket in a service
connect(url: string): Observable<unknown> { ... }
// Reconnect with exponential backoff
retry({ delay: (_, count) => timer(Math.min(1000 * 2 ** count, 30000)) })
// Unsubscribe on component destroy
.pipe(takeUntilDestroyed())
// Use wss:// in production
new WebSocket('wss://example.com/socket')
❌ Don’t Do This:
// Don't use the raw WebSocket API in components
const socket = new WebSocket(url); // ❌ no lifecycle management
// Don't subscribe without unsubscribing
this.ws.connect(url).subscribe(...); // ❌ leak
// Don't use ws:// in production
new WebSocket('ws://example.com/socket') // ❌ unencrypted
// Don't ignore close codes
socket.onclose = () => {}; // ❌ miss diagnostic info
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Subscription leak | No takeUntilDestroyed | Add takeUntilDestroyed() |
| No reconnection | retry not used | Add retry({ delay }) |
| Message lost on send | Socket not open | Queue messages until open |
| Multiple connections | share() not used | Add share() to the pipe |
| Status not updated | Using raw onclose only | Use closeObserver |
| Unencrypted connection | Using ws:// | Use wss:// in production |
Real-World Examples
1. Basic Connection
const socket = new WebSocket('wss://example.com/socket');
2. RxJS Service
private socket$ = webSocket('wss://example.com/socket');
3. Send a Message
this.socket$.next(JSON.stringify({ type: 'ping' }));
4. Receive Messages
this.socket$.subscribe((msg) => console.log(msg));
5. Retry with Backoff
retry({ delay: (_, count) => timer(Math.min(1000 * 2 ** count, 30000)) })
6. Share One Connection
share()
7. Component Subscription
this.ws.connect(url).pipe(takeUntilDestroyed()).subscribe(...)
8. Status Observable
readonly status$ = this.statusSubject.asObservable();
9. Filter by Message Type
filter((msg) => msg.type === 'price')
10. Clean Up
this.destroyRef.onDestroy(() => this.close());
Visual
HTTP vs WebSocket
┌──────────────────────────────────────────────────────────────┐
│ HTTP WebSocket │
│ │
│ Client ──request──▶ Server Client ──handshake──▶ Server│
│ Client ◀──response── Server Client ◀──frames──▶ Server │
│ Connection closes Connection stays open │
│ │
│ One request, one response Full-duplex communication │
└──────────────────────────────────────────────────────────────┘
WebSocket Lifecycle
┌──────────────────────────────────────────────────────────────┐
│ CONNECTING (0) │
│ │ │
│ ▼ │
│ OPEN (1) ──── send / receive ────▶ messages │
│ │ │
│ ▼ │
│ CLOSING (2) │
│ │ │
│ ▼ │
│ CLOSED (3) │
│ │ │
│ ▼ │
│ retry → reconnect │
└──────────────────────────────────────────────────────────────┘
Service Architecture
┌──────────────────────────────────────────────────────────────┐
│ COMPONENT │
│ │ │
│ ▼ │
│ WEBSOCKET SERVICE │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ connect(url) → Observable │ │
│ │ send(msg) → void │ │
│ │ status$ → Observable<'open' | 'closed'> │ │
│ └────────────────────────┬───────────────────────────────┘ │
│ │ │
│ ▼ │
│ WebSocketSubject (RxJS) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ retry({ delay }) │ │
│ │ share() │ │
│ └────────────────────────┬───────────────────────────────┘ │
│ │ │
│ ▼ │
│ BROWSER WebSocket API │
└──────────────────────────────────────────────────────────────┘
Reconnection with Exponential Backoff
┌──────────────────────────────────────────────────────────────┐
│ Attempt 1: fail → wait 1s │
│ Attempt 2: fail → wait 2s │
│ Attempt 3: fail → wait 4s │
│ Attempt 4: fail → wait 8s │
│ Attempt 5: fail → wait 16s │
│ Attempt 6: fail → wait 30s (capped) │
│ Attempt 7: fail → wait 30s │
│ ... │
│ │
│ Prevents hammering a server that is down. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| WebSocket protocol | Full-duplex over a single TCP connection |
| Handshake | HTTP Upgrade to 101 Switching Protocols |
| URL schemes | ws:// (unencrypted), wss:// (encrypted) |
| Browser API | WebSocket class with onopen, onmessage, onclose |
| RxJS wrapper | webSocket(url) returns WebSocketSubject |
next | Send a message |
subscribe | Receive messages |
retry | Reconnect on error |
share | Share one connection among subscribers |
takeUntilDestroyed | Unsubscribe on component destroy |
| Close code 1000 | Normal closure |
| Close code 1006 | Abnormal closure |
Key takeaways:
- WebSockets provide full-duplex communication over a single TCP connection. Both the client and the server can send messages at any time, which makes them suitable for chat, live notifications, collaborative editing, and real-time dashboards.
- The browser’s WebSocket API is callback-based.
onopen,onmessage,onerror, andoncloseare the event handlers. The API does not compose with RxJS, so wrapping it in a service is the standard Angular approach. - RxJS provides
webSocket()as a factory. It returns aWebSocketSubjectthat is both an observer (for sending) and an observable (for receiving). Theretryoperator handles reconnection, andshareensures a single connection among subscribers. - Wrap the socket in an Angular service. Components should not manage the raw socket. The service exposes an observable and a send method, and handles connection lifecycle, reconnection, and cleanup.
- Use
AsyncPipeortakeUntilDestroyedto prevent leaks. TheAsyncPipesubscribes and unsubscribes automatically. For explicit subscriptions,takeUntilDestroyedfrom@angular/core/rxjs-interopunsubscribes when the component is destroyed. - Reconnect with exponential backoff. A fixed delay hammers a server that is down. Exponential backoff doubles the delay on each attempt and caps it, giving the server time to recover.
- Use
wss://in production. An unencrypted WebSocket is readable by anything on the network path. Thewss://scheme encrypts the connection with TLS. - Track connection status for the UI. A
BehaviorSubjectupdated by the socket’sopenObserverandcloseObserverexposes astatus$observable that components can render as a connected/disconnected indicator.
Remember: WebSockets are the right tool when the server must push data or when both sides need to send messages continuously. The browser’s WebSocket API is callback-based and does not integrate with RxJS, so an Angular service wraps it and exposes an observable. The service handles the socket lifecycle, reconnection with exponential backoff, connection status, and cleanup. Components consume the observable with the AsyncPipe or an explicit subscription with takeUntilDestroyed. The share operator ensures that a single connection is shared, and the retry operator handles transient disconnections. Use wss:// in production, filter messages by type when the socket carries multiple message kinds, and always clean up on destruction. The result is a real-time Angular application where data arrives when it is available, rather than when the client happens to ask.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!