Tutorials

MQTT in Angular with ngx-mqtt and MQTT.js

Use MQTT in Angular: set up ngx-mqtt, or write your own injectable MQTT.js service with RxJS observables, signals, automatic unsubscribe and secure WebSockets.

Updated · 9 min read

MQTT in Angular with ngx-mqtt and MQTT.js

There are two good ways to use MQTT in Angular. ngx-mqtt wraps MQTT.js in an Angular module and gives you MqttService.observe(), which returns RxJS observables — the fastest way to get messages into a component. Or you write a small injectable service around MQTT.js yourself, which takes about sixty lines and gives you full control over the client version, MQTT 5 options and connection state. Both run over secure WebSockets (wss://), because browsers cannot open raw TCP connections.

This tutorial shows both, then wires the result into a standalone component with signals and tests it against a live broker.

Connection settings

SettingValue
URLwss://broker.connectmq.com:8084/mqtt
Host / port / pathbroker.connectmq.com / 8084 / /mqtt
ProtocolMQTT 3.1.1 or 5
Credentialsa browser account with access only to the UI's topics

An Angular app served over HTTPS must use wss://; ws:// is blocked as mixed content. MQTT over WebSockets explains the transport in detail.

Option 1: ngx-mqtt

Install
npm i ngx-mqtt

ngx-mqtt is maintained independently from Angular and pins its own MQTT.js dependency. Before installing, check that its peer dependencies match your Angular version; that is the most common source of install errors.

Register it once in the application config of a standalone app:

src/app/app.config.ts
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import { IMqttServiceOptions, MqttModule } from 'ngx-mqtt';

const MQTT_OPTIONS: IMqttServiceOptions = {
  hostname: 'broker.connectmq.com',
  port: 8084,
  protocol: 'wss',
  path: '/mqtt',
  username: 'username',
  password: 'password',
};

export const appConfig: ApplicationConfig = {
  providers: [importProvidersFrom(MqttModule.forRoot(MQTT_OPTIONS))],
};

Then inject MqttService wherever you need messages:

src/app/status.component.ts
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { IMqttMessage, MqttService } from 'ngx-mqtt';
import { map } from 'rxjs';

@Component({
  selector: 'app-status',
  template: `<p>Last status: {{ status() }}</p>
             <button (click)="ping()">Ping</button>`,
})
export class StatusComponent {
  private readonly mqtt = inject(MqttService);

  readonly status = toSignal(
    this.mqtt.observe('testmqtt/angular/status').pipe(map((m: IMqttMessage) => m.payload.toString())),
    { initialValue: '—' },
  );

  ping() {
    this.mqtt.unsafePublish('testmqtt/angular/cmd/ping', String(Date.now()), { qos: 1 });
  }
}

observe() shares one broker subscription between all observers of the same filter and unsubscribes when the last one goes away. publish() returns an observable that completes when the broker acknowledges the message; unsafePublish() fires and forgets.

Option 2: your own MQTT.js service

If you want the current MQTT.js release, MQTT 5 properties, or simply fewer dependencies, a hand-written service is straightforward:

Install
npm i mqtt
src/app/mqtt-client.service.ts
import { Injectable, OnDestroy, PLATFORM_ID, inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import mqtt, { type MqttClient } from 'mqtt';
import { BehaviorSubject, Observable, Subject, filter, map } from 'rxjs';
import { environment } from '../environments/environment';

export type MqttState = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'offline' | 'error';
export interface MqttMessage<T> { topic: string; data: T }

export function matches(topicFilter: string, topic: string): boolean {
  const f = topicFilter.split('/');
  const t = topic.split('/');
  for (let i = 0; i < f.length; i++) {
    if (f[i] === '#') return true;
    if (i >= t.length || (f[i] !== '+' && f[i] !== t[i])) return false;
  }
  return f.length === t.length;
}

@Injectable({ providedIn: 'root' })
export class MqttClientService implements OnDestroy {
  readonly state$ = new BehaviorSubject<MqttState>('idle');
  private readonly incoming$ = new Subject<{ topic: string; payload: Uint8Array }>();
  private readonly refCounts = new Map<string, number>();
  private client: MqttClient | null = null;

  constructor() {
    if (!isPlatformBrowser(inject(PLATFORM_ID))) return; // no MQTT during SSR
    this.state$.next('connecting');
    this.client = mqtt.connect(environment.mqtt.url, {
      username: environment.mqtt.username,
      password: environment.mqtt.password,
      clientId: `ng-${crypto.randomUUID().slice(0, 8)}`,
      reconnectPeriod: 2000,
      connectTimeout: 10_000,
    });
    this.client.on('connect', () => this.state$.next('connected'));
    this.client.on('reconnect', () => this.state$.next('reconnecting'));
    this.client.on('offline', () => this.state$.next('offline'));
    this.client.on('error', () => this.state$.next('error'));
    this.client.on('message', (topic, payload) => this.incoming$.next({ topic, payload }));
  }

  observe<T = unknown>(topicFilter: string): Observable<MqttMessage<T>> {
    return new Observable<MqttMessage<T>>((subscriber) => {
      this.retain(topicFilter);
      const sub = this.incoming$
        .pipe(
          filter((m) => matches(topicFilter, m.topic)),
          map((m) => {
            const text = new TextDecoder().decode(m.payload);
            let data: T;
            try { data = JSON.parse(text) as T; } catch { data = text as T; }
            return { topic: m.topic, data };
          }),
        )
        .subscribe(subscriber);
      return () => {
        sub.unsubscribe();
        this.release(topicFilter);
      };
    });
  }

  async publish(topic: string, message: string, retain = false): Promise<void> {
    await this.client?.publishAsync(topic, message, { qos: 1, retain });
  }

  ngOnDestroy(): void {
    this.client?.end(true);
  }

  private retain(topicFilter: string) {
    const n = this.refCounts.get(topicFilter) ?? 0;
    if (n === 0) this.client?.subscribe(topicFilter, { qos: 1 });
    this.refCounts.set(topicFilter, n + 1);
  }

  private release(topicFilter: string) {
    const n = (this.refCounts.get(topicFilter) ?? 1) - 1;
    if (n === 0) {
      this.refCounts.delete(topicFilter);
      this.client?.unsubscribe(topicFilter);
    } else {
      this.refCounts.set(topicFilter, n);
    }
  }
}

The design choices:

  • One client per app. providedIn: 'root' makes the service a singleton, so every component shares one WebSocket.
  • Cold, ref-counted observables. Nothing is subscribed on the broker until a component subscribes, and the topic is unsubscribed when the last component unsubscribes. MQTT.js re-subscribes active topics after a reconnect on its own.
  • SSR-safe. On the server the service stays idle, so Angular SSR or prerendering never opens a socket.
  • Wildcards are matched client-side. The broker sends each message once even if several filters match, so matches() routes it to the right observers. Try your filters in the topic matcher.

A component with signals

src/app/sensors.component.ts
import { Component, inject } from '@angular/core';
import { DecimalPipe } from '@angular/common';
import { toSignal } from '@angular/core/rxjs-interop';
import { auditTime, map, scan } from 'rxjs';
import { MqttClientService } from './mqtt-client.service';

interface Reading { t: number; h?: number }

@Component({
  selector: 'app-sensors',
  imports: [DecimalPipe],
  template: `
    <p>Broker: {{ state() }}</p>
    <table>
      @for (row of rows(); track row.device) {
        <tr><td>{{ row.device }}</td><td>{{ row.t | number: '1.1-1' }} °C</td></tr>
      }
    </table>
    <button [disabled]="state() !== 'connected'" (click)="ping()">Ping</button>
  `,
})
export class SensorsComponent {
  private readonly mqtt = inject(MqttClientService);

  readonly state = toSignal(this.mqtt.state$, { requireSync: true });

  readonly rows = toSignal(
    this.mqtt.observe<Reading>('testmqtt/angular/sensors/+').pipe(
      scan((acc, m) => ({ ...acc, [m.topic.split('/').pop()!]: m.data.t }), {} as Record<string, number>),
      auditTime(100),
      map((latest) => Object.entries(latest).map(([device, t]) => ({ device, t }))),
    ),
    { initialValue: [] },
  );

  ping() {
    this.mqtt.publish('testmqtt/angular/cmd/ping', String(Date.now()));
  }
}

toSignal() subscribes in the component's injection context and unsubscribes automatically when the component is destroyed — no ngOnDestroy, no takeUntil. scan keeps the latest value per device, and auditTime(100) limits UI updates to ten per second however fast messages arrive, which keeps change detection cheap on busy topics. (On Angular versions before 19, add standalone: true to the decorator.)

If you prefer observables in templates, observe() works just as well with the async pipe or with takeUntilDestroyed() in a service.

Testing

Manual end-to-end check

  1. Run ng serve and confirm the component shows Broker: connected. If it shows error, open the browser console: Connection refused: Not authorized means credentials or ACLs; a failed WebSocket handshake means URL, port 8084 or path /mqtt.
  2. Open the online MQTT client, connect to the same broker and publish {"t": 23.4} to testmqtt/angular/sensors/room-1. The row appears within 100 ms.
  3. Publish to testmqtt/angular/sensors/room-2 and check that both rows are shown.
  4. Subscribe to testmqtt/angular/# in the online client and click Ping to verify publishing.
  5. Route away from the component and, in a debugger or the broker log, confirm the topic is unsubscribed.

Publish test readings to your Angular app

Unit tests

Components should not need a broker. Replace the service with a stub backed by a Subject:

src/app/sensors.component.spec.ts
import { TestBed } from '@angular/core/testing';
import { BehaviorSubject, Subject } from 'rxjs';
import { MqttClientService, MqttMessage } from './mqtt-client.service';
import { SensorsComponent } from './sensors.component';

it('renders the latest reading per device', async () => {
  const messages = new Subject<MqttMessage<{ t: number }>>();
  await TestBed.configureTestingModule({
    imports: [SensorsComponent],
    providers: [{
      provide: MqttClientService,
      useValue: { state$: new BehaviorSubject('connected'), observe: () => messages, publish: async () => {} },
    }],
  }).compileComponents();

  const fixture = TestBed.createComponent(SensorsComponent);
  fixture.detectChanges();
  messages.next({ topic: 'testmqtt/angular/sensors/room-1', data: { t: 21 } });
  await new Promise((r) => setTimeout(r, 150)); // wait for auditTime
  fixture.detectChanges();
  expect(fixture.nativeElement.textContent).toContain('room-1');
});

Test the service itself, including matches() and reconnect behavior, against a real broker in a small integration suite. The Jest testing guide shows patterns that carry over to Angular test runners, and MQTT with TypeScript covers typing payloads end to end.

Troubleshooting

SymptomCauseFix
npm i ngx-mqtt fails with a peer dependency conflictLibrary not yet updated for your Angular versionUse the MQTT.js service from option 2
SSR build hangs or times outClient connects on the serverGuard with isPlatformBrowser or afterNextRender()
UI lags on busy topicsChange detection on every messageThrottle with auditTime or throttleTime
Tabs disconnect each otherSame clientId everywhereGenerate a random client ID per tab

For more error strings, see MQTT.js errors.

Angular and MQTT fit together naturally because both think in streams: a singleton service owns the connection, components consume observables or signals, and cleanup happens when the component goes away. Start with ngx-mqtt if it supports your Angular version, switch to the MQTT.js service when you need more control, and verify every topic with a second client before you ship.

Frequently asked questions

Should I use ngx-mqtt or MQTT.js directly in Angular?

ngx-mqtt is the quickest start: one forRoot() call and an observe() method that returns RxJS observables. A small service around MQTT.js gives you the latest MQTT.js, MQTT 5 options and full control, and does not depend on a wrapper keeping pace with Angular releases.

Do I need to unsubscribe from MQTT observables in Angular components?

Yes, or use something that does it for you. toSignal(), the async pipe and takeUntilDestroyed() all unsubscribe when the component is destroyed. With a ref-counted observe() method that also unsubscribes the topic on the broker once nobody listens.

Why does Angular SSR hang or error when my app uses MQTT?

The MQTT client tries to open a WebSocket on the server and keeps the render from finishing. Only connect in the browser: check isPlatformBrowser(PLATFORM_ID) before connecting, or start the connection in afterNextRender().