The core issue

It’s quite common to see the terms unsubscribe, memory leaks, and subject appear together in discussions about RxJS. This piece aims to clarify why those leaks occur and why a straightforward unsubscribe() call is the remedy. While prior exposure to RxJS is helpful, the essential ideas will be explained as we go. For deeper reading on RxJS subjects, this resource is a good starting point, along with another on AsyncSubject.

This content draws from a detailed answer on Stack Overflow.

Exploring the source of the leak

RxJS works independently, but for this discussion we’ll frame the problem within Angular, simply because it’s a familiar context. The underlying principles hold across any environment.

Let’s start with a minimal RxJS demonstration of a memory leak, then build on it:

const source = new Subject();
 
let s = source.subscribe(v => console.log("subscriber 1: ", v));
 
source.next("1"); // logs: subscriber 1: 1
 
// this won't do anything
s = null
 
// notice we didn’t unsubscribe before
s = source.subscribe(v => console.log("subscriber 2: ", v));
 
source.next("2");
// logs:
// subscriber 1: 2 // !!! - this shouldn't be here
// subscriber 2: 2

You can try the snippet above in this StackBlitz project.

In Angular, a typical approach involves injecting a service into a component and subscribing to one of its observable properties:

class Service {
 private usersSrc = new Subject();
 users$ = this.usersSrc.asObservable();
}

Such a service would be used in the following manner:

class FooComponent {
 constructor (private service: Service) { }
  ngOnInit () {
   this.subscription = this.service.users$.subscribe(nextCb, errorCb, completeCb)
 }
}

As is well known, a Subject is a particular variety of Observable with notable capabilities. The key one for us is that it supports subscriptions and internally tracks all of them. The mechanics of subscribing could fill an entire article, but what matters here is that calling subject.next() pushes the value to every active subscriber. A subscriber gets registered the moment subscribe(nextCb, ...) is invoked, and from then on, each emitted value reaches nextCb.

Put simply, the subject hangs on to every callback it receives.

Now, if a component is removed from the view—say, due to a route change—and we skip this.subscription.unsubscribe(), that subscriber stays in the subject’s internal list. The role of unsubscribe is to remove the subscriber from that collection. This distinction matters, because each time the component is recreated and ngOnInit runs, a new subscriber is appended to the list. Without this.subscription.unsubscribe(), the old one lingers alongside it.

Below is a condensed version of how a leak develops:

// the Subject used in the service
let src = {
 subscribers: [],
 
 addSubscriber(cb) {
   this.subscribers.push(cb);
   return this.subscribers.length - 1;
 },
 
 removeSubscriber(idx) {
   this.subscribers.splice(idx, 1);
 },
 next(data) {
   this.subscribers.forEach(cb => cb(data));
 }
};

And here’s what a basic component might look like in that scenario:

// the component
class Foo {
 subIdx: number;
 constructor() {
   this.subIdx = src.addSubscriber(value => {
     console.log(value);
   });
 }
 
 onDestroy() {
   // the equivalent of `unsubscribe()`
   src.removeSubscriber(this.subIdx);
 }
}
 
// creating a new component
let foo = new Foo(); // Foo {subIdx: 0}
 
// sending data to subscribers
src.next("sending data for the first time");
// console output: `sending data for the first time`
 
// destroying the component - without calling `onDestroy`
foo = null;
 
src.next("sending data for the second time"); // the subscriber is still there
// console output: `sending data for the second time`
 
// registering a new instance - Foo {subIdx: 1}
// at this point, a new subscriber has been created
foo = new Foo();
 
src.next("sending data for the third time");
// console output: `sending data for the third time`
// console output: `sending data for the third time`

Feel free to experiment with this code in a StackBlitz sandbox.

After src.next('test2') runs, you’ll notice 'foo' appears twice, which is the telltale sign of a leak. The behavior is analogous with Subjects and their subscribers.

These issues tend to surface when the source is unbounded—it never produces a complete or error event, such as a global service shared across components. However, there are cases where unsubscribing is optional. For instance, if the Subject becomes unreachable once its owner is destroyed or set to null, that’s what happens with ActivatedRoute or form control Subjects (valueChanges, statusChanges) upon component teardown.

Wrapping up

This brief overview should make the reason behind RxJS Subject memory leaks clearer. In essence, a Subject holds onto subscribers through a list, and when a subscriber is no longer needed, its unsubscribe() method must be invoked. Skipping that step leads to unintended side effects.

Thanks for reading!