RxJS memory leaks in Angular: find and fix leaked subscriptions
An RxJS memory leak happens when a subscription outlives the component or service that created it. The stream keeps emitting, the callback keeps running, and the references it holds never get garbage-collected. Here's what causes leaked subscriptions, how to find them fast, and how to stop them coming back.
What is an RxJS memory leak?
A leak is a subscription that is never unsubscribed. When you call observable.subscribe(...), RxJS keeps the observer alive until the stream completes or you tear it down. If the owning component is destroyed but the subscription isn't, the closure — and everything it captured (DOM nodes, services, large objects) — stays reachable. Navigate in and out of that component a few hundred times and memory climbs, change detection slows, and stale callbacks fire on dead components.
Why RxJS subscriptions leak
The usual culprits:
- Infinite streams subscribed manually —
valueChanges,fromEvent,interval, route params, NgRx selectors. They never complete on their own. - A forgotten teardown — no
takeUntilDestroyed(), nounsubscribe()inngOnDestroy. - Subscriptions in services — singletons with no obvious lifecycle, so nobody adds teardown.
- Nested subscribes —
subscribe()insidesubscribe()instead of a flattening operator; the inner one is easy to lose track of. - Operator placed after the leak — e.g.
takeUntilpiped before aswitchMapthat re-subscribes.
How to find leaked subscriptions
Option A — DevTools heap snapshots (manual)
Take a heap snapshot, exercise the suspect route several times, take another, and diff. Filter for Subscriber / SafeSubscriber instances that keep growing. It works, but it's slow, noisy, and tells you a leak exists without telling you where it was created.
Option B — rxjs-leak-finder (one line)
rxjs-leak-finder patches the RxJS Observable prototype in dev mode and records a stack trace at every subscribe(). When a subscription's owner is destroyed but the subscription is still open, it's reported in a dashboard with the exact call site — so you jump straight to the line that leaked.
// main.ts (dev only)
import { isDevMode } from '@angular/core';
import { enableRxjsLeakDetector } from 'rxjs-leak-finder';
if (isDevMode()) {
enableRxjsLeakDetector();
} No Chrome extension, ~5 KB, and completely inert in production because it's gated behind isDevMode(). Try it in the playground without installing anything.
How to fix a leaked subscription
- Prefer the
asyncpipe — it subscribes and unsubscribes for you. The best leak is the subscription you never wrote. - Pipe
takeUntilDestroyed()on manual subscriptions (Angular 16+). See takeUntil vs takeUntilDestroyed. - Tear down in
ngOnDestroywhen you must subscribe imperatively — or collect them, see SubSink. - Re-run the detector to confirm the leak is gone, not just moved.
How to prevent them long-term
Pick one teardown convention and apply it everywhere — takeUntilDestroyed, until-destroy, or the async pipe — then run rxjs-leak-finder in dev as the safety net that proves the convention is actually being followed. Prevention assumes discipline; detection verifies it. For the full teardown playbook, see how to unsubscribe in Angular.
FAQ
Do RxJS subscriptions always leak if I don't unsubscribe?
No. Streams that complete (an HTTP call, a stream piped through take(1) or first()) clean themselves up. Leaks come from long-lived streams you subscribe to manually and never end.
Does the async pipe prevent memory leaks?
Yes, for template-bound streams — Angular unsubscribes when the view is destroyed. The leaks that remain are the ones you subscribe to in TypeScript.