Interview answer drill

Use this Angular interview question to rehearse a quick answer, common mistake, follow-up, and production pitfall.

How Do You Prevent Memory Leaks in Angular? All Real-World Unsubscribe Patterns ExplainedFrontend interview answer

HighIntermediateAngular
Interview focus

This Angular interview question tests whether you can explain Angular memory leaks in production: long-lived streams, AsyncPipe, and teardown patterns, connect it to production trade-offs, and handle common follow-up questions.

  • Angular memory leaks in production: long-lived streams, AsyncPipe, and teardown patterns explanation without falling back to memorized docs wording
  • RxJS and Subscriptions reasoning, edge cases, and production failure modes
  • How you would answer the most likely Angular interview follow-up
Practice more Angular interview questions
Interview quick answer

Most Angular memory leaks come from long-lived streams, global listeners, or timers that outlive a component or route. The practical angle is knowing when AsyncPipe, takeUntilDestroyed, or manual teardown actually matters, when HTTP completes on its own, and how never-ending resolver streams can block navigation.

Full interview answer

Teardown mental model

Most Angular memory leaks are not mysterious framework bugs. They come from long-lived streams, timers, or global listeners that outlive a component. The production question is not “unsubscribe from everything?” but “which sources complete on their own, and which ones keep consuming memory, CPU, or network until I tear them down?”

Source type

Does it auto-complete?

Needs manual unsubscribe?

Why

HttpClient request

✅ Yes

❌ No

HTTP observables complete after one response.

ActivatedRoute params, valueChanges, Subjects

❌ No

✅ Yes

They are long-lived streams tied to app lifetime.

interval, fromEvent, WebSocket

❌ No

✅ Yes

They never complete unless you stop them.

async pipe

❌ No

Angular unsubscribes automatically on destroy.

Which subscriptions leak if you forget to clean them up?

Pattern #1: Use the async pipe (best default)

The async pipe automatically subscribes and unsubscribes when the component is destroyed. It also integrates perfectly with OnPush change detection.

TYPESCRIPT
@Component({
  selector: 'app-users',
  template: `
    <ul>
      <li *ngFor="let u of users$ | async">{{ u.name }}</li>
    </ul>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UsersComponent {
  users$ = this.api.loadUsers(); // no manual subscribe
}
                  

Pattern #2: takeUntilDestroyed (modern Angular, recommended)

This is the cleanest way to auto-unsubscribe in code in Angular 16+ using DestroyRef.

TYPESCRIPT
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({ selector: 'app-demo', template: '' })
export class DemoComponent {
  private destroyRef = inject(DestroyRef);

  ngOnInit() {
    this.someStream$
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(value => {
        // safe: auto-unsubscribed on destroy
      });
  }
}
                  

Outside an injection context

takeUntilDestroyed() is still usable when the subscription setup happens in a helper or service method, but then you must pass an explicit DestroyRef. That detail matters when you refactor teardown logic out of the component body.

TYPESCRIPT
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Observable } from 'rxjs';

export class SearchFacade {
  connect(stream$: Observable<string>, destroyRef: DestroyRef) {
    return stream$.pipe(takeUntilDestroyed(destroyRef));
  }
}

@Component({ selector: 'app-search', template: '' })
export class SearchComponent {
  private destroyRef = inject(DestroyRef);

  ngOnInit() {
    this.facade.connect(this.form.valueChanges, this.destroyRef)
      .subscribe();
  }
}
                  

Pattern #3: Classic takeUntil + Subject (works in all Angular versions)

TYPESCRIPT
import { Component, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Component({ selector: 'app-demo', template: '' })
export class DemoComponent implements OnDestroy {
  private destroy$ = new Subject<void>();

  ngOnInit() {
    this.someStream$
      .pipe(takeUntil(this.destroy$))
      .subscribe(value => {
        // safe
      });
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}
                  

Pattern #4: Manual Subscription aggregation (old-school)

Works, but easy to forget to add new subscriptions. Not recommended for large components.

TYPESCRIPT
import { Component, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';

@Component({ selector: 'app-demo', template: '' })
export class DemoComponent implements OnDestroy {
  private sub = new Subscription();

  ngOnInit() {
    this.sub.add(this.someStream$.subscribe());
    this.sub.add(this.otherStream$.subscribe());
  }

  ngOnDestroy() {
    this.sub.unsubscribe();
  }
}
                  

Non-RxJS leaks: event listeners & timers

Leaks don’t only come from Observables. addEventListener, setInterval, and 3rd-party libraries must also be cleaned up in ngOnDestroy.

TYPESCRIPT
ngOnInit() {
  window.addEventListener('resize', this.onResize);
  this.timer = setInterval(() => {}, 1000);
}

ngOnDestroy() {
  window.removeEventListener('resize', this.onResize);
  clearInterval(this.timer);
}
                  

Route-level pitfall: resolver streams that never complete

A resolver is not just another subscription site. If the resolver returns a stream that never completes, navigation can hang forever because the router is still waiting for resolution. That is a different failure mode from a component leak, but it comes from the same teardown misunderstanding.

TYPESCRIPT
export const projectResolver: ResolveFn<Project> = route => {
  const api = inject(ProjectApi);

  // ✅ one-shot HTTP completes, so navigation can continue
  return api.loadProject(route.paramMap.get('projectId')!);

  // ❌ dangerous if returned directly:
  // return api.projectUpdates$;
  // A never-ending stream can keep the route unresolved forever.
};
                  

Pattern

When to use

Why

async pipe

Template-driven streams

Safest, zero-leak, integrates with OnPush.

takeUntilDestroyed

Subscriptions in component code (Angular 16+)

Modern, concise, no boilerplate.

takeUntil + Subject

Older Angular or shared base classes

Battle-tested and explicit.

Manual Subscription

Very small/simple components

Easy to forget and cause leaks.

Which unsubscribe strategy should you pick?

Senior pitfalls

• Assuming Angular auto-unsubscribes everything (it doesn’t)
• Forgetting that ActivatedRoute, valueChanges, Subjects never complete
• Subscribing in services that live forever (root scope)
• Returning never-ending resolver streams and blocking navigation
• Leaking intervals, event listeners, and WebSocket connections

Interview summary

To prevent memory leaks in Angular: prefer the async pipe in templates, use takeUntilDestroyed (or takeUntil + Subject) in code, know which Observables complete automatically (HTTP) and which do not, and clean up timers, event listeners, and route-level long-lived streams deliberately. The hidden follow-up is that resolvers must usually complete, not just unsubscribe eventually.

Similar questions
Guides
Preparing for interviews?

Use this as one explanation rep, then continue with the Angular interview questions cluster or a guided prep path.