| |

Angular 120 🅰️ Angular and WebSockets

⏱️ Estimated read: 15 min | |

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.

MemberTypePurpose
readyStatenumberConnection state: 0–3
send(data)methodSend a message
close(code, reason)methodClose the connection
onopeneventConnection established
onmessageeventMessage received
onerroreventError occurred
oncloseeventConnection 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

MemberPurpose
new WebSocket(url)Create connection
readyState0 = CONNECTING, 1 = OPEN, 2 = CLOSING, 3 = CLOSED
send(data)Send a message
close(code, reason)Close the connection
onopenConnection established
onmessageMessage received
onerrorError occurred
oncloseConnection closed

RxJS WebSocket API

APIPurpose
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

CodeMeaning
1000Normal closure
1001Going away
1006Abnormal closure (no close frame)
1011Server error
4000–4999Application-defined

Angular Integration

PatternPurpose
Service with webSocket()Encapsulate the socket
AsyncPipe on the observableSubscribe and unsubscribe automatically
takeUntilDestroyed()Unsubscribe on component destroy
BehaviorSubject for statusExpose 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

PitfallWhy It HappensFix
Subscription leakNo takeUntilDestroyedAdd takeUntilDestroyed()
No reconnectionretry not usedAdd retry({ delay })
Message lost on sendSocket not openQueue messages until open
Multiple connectionsshare() not usedAdd share() to the pipe
Status not updatedUsing raw onclose onlyUse closeObserver
Unencrypted connectionUsing 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

ItemValue
WebSocket protocolFull-duplex over a single TCP connection
HandshakeHTTP Upgrade to 101 Switching Protocols
URL schemesws:// (unencrypted), wss:// (encrypted)
Browser APIWebSocket class with onopen, onmessage, onclose
RxJS wrapperwebSocket(url) returns WebSocketSubject
nextSend a message
subscribeReceive messages
retryReconnect on error
shareShare one connection among subscribers
takeUntilDestroyedUnsubscribe on component destroy
Close code 1000Normal closure
Close code 1006Abnormal 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, and onclose are 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 a WebSocketSubject that is both an observer (for sending) and an observable (for receiving). The retry operator handles reconnection, and share ensures 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 AsyncPipe or takeUntilDestroyed to prevent leaks. The AsyncPipe subscribes and unsubscribes automatically. For explicit subscriptions, takeUntilDestroyed from @angular/core/rxjs-interop unsubscribes 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. The wss:// scheme encrypts the connection with TLS.
  • Track connection status for the UI. A BehaviorSubject updated by the socket’s openObserver and closeObserver exposes a status$ 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.


Was this helpful?


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!