← Back to Articles         
Preventing RxJS Subscription Memory Leaks

Preventing RxJS Subscription Memory Leaks

Published on 01 Oct 2026     11 min read Angular
RxJS & Observables

Preventing RxJS Subscription Memory Leaks & takeUntilDestroyed()

These two questions are closely related.

The main idea is:

A subscription can become a problem when it remains active after the component no longer needs it.

Modern Angular provides several ways to manage this, and takeUntilDestroyed() is one of the most convenient for subscriptions created in component/directive code.


1. How Do You Prevent Subscription Memory Leaks?

What is a subscription memory leak?

When you call:

observable$.subscribe(...)

RxJS creates a subscription.

For a long-lived Observable, that subscription can continue receiving values until:

  • the Observable completes,
  • the Observable errors,
  • or the subscription is unsubscribed.

If a component is destroyed but an unnecessary subscription remains active, it can keep executing callbacks and retaining resources.

Example:

interval(1000)

doesn't complete by itself.

If we subscribe:

interval(1000)
  .subscribe(value => {
    console.log(value);
  });

it continues:

0
1
2
3
4
5
...

If that subscription outlives the component, it can become a leak.


Full Example — Problem

timer-demo.ts

import { Component, OnInit } from '@angular/core';
import { interval } from 'rxjs';

@Component({
  selector: 'app-timer-demo',
  standalone: true,
  template: `
    <h2>Timer</h2>
  `
})
export class TimerDemo implements OnInit {

  ngOnInit(): void {

    interval(1000)
      .subscribe(value => {

        console.log(value);

      });
  }
}

The Observable:

interval(1000)
     │
     ▼
0 → 1 → 2 → 3 → 4 → 5 → ...

If the component is removed, we want its subscription to stop.


2. Common Ways to Prevent Subscription Leaks

The main approaches are:

Method Best use
AsyncPipe Observable values displayed in templates
takeUntilDestroyed() Imperative subscriptions in Angular code
take(1) Only one emission is needed
first() First matching/available emission
Manual unsubscribe() When explicit subscription control is required
takeUntil() Older/custom notifier-based lifecycle pattern

For modern Angular component code, two especially useful approaches are:

Template subscription
      ↓
AsyncPipe

TypeScript subscription
      ↓
takeUntilDestroyed()

3. Method 1 — Use AsyncPipe

If the Observable exists mainly to display data in the template, AsyncPipe is usually the cleanest option.

Component

import { AsyncPipe } from '@angular/common';
import { Component } from '@angular/core';
import { interval } from 'rxjs';

@Component({
  selector: 'app-timer',
  standalone: true,
  imports: [AsyncPipe],
  template: `
    <h2>
      Counter: {{ counter$ | async }}
    </h2>
  `
})
export class Timer {

  counter$ = interval(1000);
}

No:

.subscribe(...)

is required in the component.

Angular's AsyncPipe manages the subscription lifecycle.

Component created
      │
      ▼
AsyncPipe subscribes
      │
      ▼
0 → 1 → 2 → 3...
      │
      ▼
Component/View destroyed
      │
      ▼
AsyncPipe unsubscribes

4. Method 2 — takeUntilDestroyed()

This is the main answer to your second question.

What is takeUntilDestroyed()?

takeUntilDestroyed() is an Angular RxJS interop operator that automatically completes/unsubscribes the subscription chain when the associated Angular context is destroyed.

It comes from:

import {
  takeUntilDestroyed
} from '@angular/core/rxjs-interop';

Simple definition

takeUntilDestroyed() automatically stops an Observable subscription when the Angular component/directive is destroyed.


5. Full Simple takeUntilDestroyed() Example

import {
  Component,
  DestroyRef,
  inject
} from '@angular/core';

import { interval } from 'rxjs';

import {
  takeUntilDestroyed
} from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-timer',
  standalone: true,
  template: `
    <h2>Timer Example</h2>

    <p>{{ counter }}</p>
  `
})
export class Timer {

  private destroyRef = inject(DestroyRef);

  counter = 0;

  constructor() {

    interval(1000)
      .pipe(
        takeUntilDestroyed(this.destroyRef)
      )
      .subscribe(value => {

        this.counter = value;

      });
  }
}

6. How Does takeUntilDestroyed() Work?

We have:

interval(1000)

which produces:

0 → 1 → 2 → 3 → 4 → 5 → ...

Then:

.pipe(
  takeUntilDestroyed(this.destroyRef)
)

connects the Observable lifecycle to the Angular destruction lifecycle.

Conceptually:

Component created
      │
      ▼
Subscribe
      │
      ▼
interval()
      │
      ▼
0 → 1 → 2 → 3 → ...
      │
Component destroyed
      │
      ▼
DestroyRef triggered
      │
      ▼
takeUntilDestroyed()
      │
      ▼
Subscription stops

You don't have to manually write:

ngOnDestroy()

just for this subscription cleanup.


7. What is DestroyRef?

DestroyRef provides a way to register work associated with the destruction of the current Angular lifecycle context.

We can inject it:

private destroyRef = inject(DestroyRef);

Then:

takeUntilDestroyed(this.destroyRef)

knows when that context is destroyed.

Think of it as:

DestroyRef
    │
    ▼
"Tell me when this Angular context
is being destroyed."

takeUntilDestroyed() uses that lifecycle signal to terminate the Observable subscription.


8. Can We Use takeUntilDestroyed() Without Passing DestroyRef?

Yes, when it is called within an Angular injection context, you can write:

interval(1000)
  .pipe(
    takeUntilDestroyed()
  )
  .subscribe(...);

For example, using it during construction:

import { Component } from '@angular/core';
import { interval } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-timer',
  standalone: true,
  template: `
    <p>{{ counter }}</p>
  `
})
export class Timer {

  counter = 0;

  constructor() {

    interval(1000)
      .pipe(
        takeUntilDestroyed()
      )
      .subscribe(value => {

        this.counter = value;

      });
  }
}

Angular can obtain the destruction context automatically there.


9. What About ngOnInit()?

This distinction is important.

If you want to use it in a method such as:

ngOnInit()

a clear approach is to inject DestroyRef and pass it explicitly.

Full Example

import {
  Component,
  DestroyRef,
  inject,
  OnInit
} from '@angular/core';

import { interval } from 'rxjs';

import {
  takeUntilDestroyed
} from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-timer',
  standalone: true,
  template: `
    <h2>Counter</h2>

    <p>{{ counter }}</p>
  `
})
export class Timer implements OnInit {

  private destroyRef = inject(DestroyRef);

  counter = 0;

  ngOnInit(): void {

    interval(1000)
      .pipe(
        takeUntilDestroyed(this.destroyRef)
      )
      .subscribe(value => {

        this.counter = value;

      });
  }
}

This is explicit and easy to understand:

takeUntilDestroyed(this.destroyRef)

10. Old Approach — Manual unsubscribe()

Before these newer lifecycle helpers, you would commonly store the subscription.

import {
  Component,
  OnDestroy,
  OnInit
} from '@angular/core';

import {
  interval,
  Subscription
} from 'rxjs';

@Component({
  selector: 'app-timer',
  standalone: true,
  template: `
    <p>{{ counter }}</p>
  `
})
export class Timer implements OnInit, OnDestroy {

  counter = 0;

  private subscription!: Subscription;

  ngOnInit(): void {

    this.subscription =
      interval(1000)
        .subscribe(value => {

          this.counter = value;

        });
  }

  ngOnDestroy(): void {

    this.subscription.unsubscribe();

  }
}

Flow:

ngOnInit()
    │
    ▼
subscribe()
    │
    ▼
Subscription stored
    │
    ▼
Component destroyed
    │
    ▼
ngOnDestroy()
    │
    ▼
unsubscribe()

This works, but requires more manual code.


11. Old Common Pattern — takeUntil()

Another widely used pattern is:

private destroy$ = new Subject<void>();

Then:

observable$
  .pipe(
    takeUntil(this.destroy$)
  )
  .subscribe(...);

Finally:

ngOnDestroy(): void {

  this.destroy$.next();
  this.destroy$.complete();

}

Full Example

import {
  Component,
  OnDestroy,
  OnInit
} from '@angular/core';

import {
  interval,
  Subject,
  takeUntil
} from 'rxjs';

@Component({
  selector: 'app-timer',
  standalone: true,
  template: `
    <p>{{ counter }}</p>
  `
})
export class Timer implements OnInit, OnDestroy {

  private destroy$ =
    new Subject<void>();

  counter = 0;

  ngOnInit(): void {

    interval(1000)
      .pipe(
        takeUntil(this.destroy$)
      )
      .subscribe(value => {

        this.counter = value;

      });
  }

  ngOnDestroy(): void {

    this.destroy$.next();

    this.destroy$.complete();

  }
}

This is still valid, and you'll see it in many existing Angular projects.


12. takeUntil() vs takeUntilDestroyed()

The older pattern requires:

private destroy$ =
  new Subject<void>();

plus:

ngOnDestroy(): void {

  this.destroy$.next();
  this.destroy$.complete();

}

With takeUntilDestroyed():

private destroyRef = inject(DestroyRef);

and:

observable$
  .pipe(
    takeUntilDestroyed(this.destroyRef)
  )
  .subscribe(...);

No dedicated destroy$ Subject is necessary.

Comparison

takeUntil() takeUntilDestroyed()
RxJS operator Angular RxJS interop operator
Requires notifier Observable Uses Angular destruction lifecycle
Often uses destroy$ Subject No destroy$ needed
Often needs ngOnDestroy() Usually no cleanup hook needed
More boilerplate Less boilerplate
Common in older/existing code Convenient modern Angular approach

13. take(1)

Sometimes you know that you only need the first emission.

observable$
  .pipe(
    take(1)
  )
  .subscribe(value => {

    console.log(value);

  });

After one value:

Observable

A → B → C → D
│
▼
take(1)
│
▼
A
│
▼
Complete

Because the chain completes after one value, it doesn't remain subscribed indefinitely.

But don't add take(1) merely as a generic cleanup mechanism if the stream logically needs multiple values.


14. Do Angular HttpClient Subscriptions Always Need Manual Cleanup?

Usually, a normal one-shot HTTP request completes automatically.

For example:

this.http
  .get<Product[]>('/api/products')
  .subscribe(products => {

    console.log(products);

  });

The normal lifecycle is:

Subscribe
    │
    ▼
HTTP request
    │
    ▼
Response
    │
    ▼
next()
    │
    ▼
complete()

Once completed, that subscription is no longer an ongoing stream.

So a simple finite HttpClient request is different from a long-lived stream such as:

interval()

or other streams that can remain active.

However, lifecycle-bound cancellation can still be useful for in-flight requests when the component disappears, and takeUntilDestroyed() provides a consistent way to do that.


15. Example: Multiple Subscriptions

Suppose your component has:

this.route.params

and:

this.form.valueChanges

and:

interval(1000)

You can apply the same lifecycle rule to each:

this.route.params
  .pipe(
    takeUntilDestroyed(this.destroyRef)
  )
  .subscribe(...);


this.form.valueChanges
  .pipe(
    takeUntilDestroyed(this.destroyRef)
  )
  .subscribe(...);


interval(1000)
  .pipe(
    takeUntilDestroyed(this.destroyRef)
  )
  .subscribe(...);

When the component is destroyed:

Component destroyed
        │
        ▼
     DestroyRef
        │
        ▼
takeUntilDestroyed()
        │
        ├── Route subscription stops
        ├── Form subscription stops
        └── Timer subscription stops

This makes lifecycle management much simpler.


16. AsyncPipe vs takeUntilDestroyed()

This is a useful interview distinction.

AsyncPipe

Use primarily when the Observable is consumed directly by the template.

products$ =
  this.productService.getProducts();
@if (products$ | async; as products) {
  ...
}

takeUntilDestroyed()

Use when you need an explicit subscription in TypeScript:

this.form.valueChanges
  .pipe(
    takeUntilDestroyed(this.destroyRef)
  )
  .subscribe(value => {

    console.log(value);

  });

Easy rule

Need value in HTML?
      │
      ▼
   AsyncPipe


Need explicit subscribe()
in TypeScript?
      │
      ▼
takeUntilDestroyed()

This isn't an absolute rule, but it's a very useful default.


17. Memory Leak Example

Consider:

interval(1000)
  .subscribe(value => {

    console.log(value);

  });

User navigates:

Products Component
       │
       ▼
Subscription #1
       │
       ▼
Navigate away
       │
       ▼
Subscription #1 may still run

Then the component is opened again:

Products Component
       │
       ▼
Subscription #2

If old subscriptions aren't cleaned up, repeated navigation can create unwanted active subscriptions:

Subscription #1 ─────► running

Subscription #2 ─────► running

Subscription #3 ─────► running

This can cause:

  • unnecessary CPU work,
  • duplicate callbacks,
  • duplicate operations,
  • retained objects/resources,
  • unexpected application behavior.

18. Fixed with takeUntilDestroyed()

interval(1000)
  .pipe(
    takeUntilDestroyed(this.destroyRef)
  )
  .subscribe(value => {

    console.log(value);

  });

Now:

Component #1
    │
    ▼
Subscription #1
    │
    ▼
Component destroyed
    │
    ▼
Subscription #1 stops


Component #2
    │
    ▼
Subscription #2

The subscription lifetime follows the component lifetime.


Key Points

  • A subscription leak can occur when an unnecessary long-lived subscription remains active after its owner is destroyed.
  • Not every subscription requires manual cleanup; finite streams that complete naturally don't remain active forever.
  • AsyncPipe manages subscriptions automatically for template consumption.
  • takeUntilDestroyed() is a convenient modern Angular approach for lifecycle-bound subscriptions in code.
  • takeUntilDestroyed() is imported from:
@angular/core/rxjs-interop
  • It works with Angular's destruction lifecycle.
  • DestroyRef can be passed explicitly:
takeUntilDestroyed(this.destroyRef)
  • takeUntil() with a destroy$ Subject is a common older/existing pattern.
  • take(1) is useful when the stream logically requires only one emission.
  • Normal Angular HttpClient requests generally complete after the response.
  • Long-lived streams such as interval(), valueChanges, event streams, and similar sources deserve particular attention.

Easy memory rule

Template Observable
        │
        ▼
    AsyncPipe


Explicit subscription
in Angular class
        │
        ▼
takeUntilDestroyed()


Only one value needed
        │
        ▼
      take(1)

Interview Questions and Answers

1. How do you prevent RxJS subscription memory leaks in Angular?

Answer:
Use lifecycle-aware techniques such as AsyncPipe for template subscriptions and takeUntilDestroyed() for explicit subscriptions. For streams that logically need only one value, operators such as take(1) can also complete the subscription automatically.


2. What is takeUntilDestroyed()?

Answer:
takeUntilDestroyed() is an Angular RxJS interop operator that completes an Observable chain when the associated Angular destruction context is destroyed.

observable$
  .pipe(
    takeUntilDestroyed(this.destroyRef)
  )
  .subscribe(...);

3. Where is takeUntilDestroyed() imported from?

import {
  takeUntilDestroyed
} from '@angular/core/rxjs-interop';

4. What is DestroyRef?

Answer:
DestroyRef represents Angular's destruction lifecycle for the current context and allows cleanup behavior to be associated with that lifecycle.

private destroyRef =
  inject(DestroyRef);

5. Do we need ngOnDestroy() with takeUntilDestroyed()?

Answer:
Usually no. One of its main advantages is that you don't need to create a destroy$ Subject or manually implement ngOnDestroy() solely for subscription cleanup.


6. What is the difference between takeUntil() and takeUntilDestroyed()?

Answer:

takeUntil()
→ RxJS operator
→ Requires a notifier Observable
→ Often uses destroy$ + ngOnDestroy()


takeUntilDestroyed()
→ Angular RxJS interop
→ Uses Angular destruction lifecycle
→ Less lifecycle boilerplate

7. AsyncPipe vs takeUntilDestroyed()?

Answer:
Use AsyncPipe when the Observable is primarily consumed in the template. Use takeUntilDestroyed() when you need an explicit subscription in Angular TypeScript code that should stop with the component/directive lifecycle.


8. Do HttpClient subscriptions need to be manually unsubscribed?

Answer:
Normal one-shot HttpClient Observables usually complete after receiving their response, so they generally don't remain as long-lived subscriptions. Lifecycle cancellation can still be useful when an in-flight request is no longer needed.


9. Which subscriptions are more likely to require lifecycle management?

Answer:
Long-lived streams such as:

interval()
FormControl.valueChanges
Router/event streams
Subjects
WebSocket/event streams
Custom non-completing Observables

should be managed appropriately when explicitly subscribed to.


10. Short Interview Answer

To prevent subscription leaks in Angular, prefer AsyncPipe for template-bound Observables and takeUntilDestroyed() for lifecycle-bound explicit subscriptions. takeUntilDestroyed() ties the Observable subscription to Angular's destruction lifecycle, so it stops automatically when the component or directive is destroyed.

← Back to Articles