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.
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
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
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 |
|---|---|---|---|
| ✅ Yes | ❌ No | HTTP observables complete after one response. |
| ❌ No | ✅ Yes | They are long-lived streams tied to app lifetime. |
| ❌ No | ✅ Yes | They never complete unless you stop them. |
| — | ❌ No | Angular unsubscribes automatically on destroy. |
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.
@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.
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 contexttakeUntilDestroyed() 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.
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)
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.
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.
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.
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 |
|---|---|---|
| Template-driven streams | Safest, zero-leak, integrates with OnPush. |
| Subscriptions in component code (Angular 16+) | Modern, concise, no boilerplate. |
| Older Angular or shared base classes | Battle-tested and explicit. |
Manual Subscription | Very small/simple components | Easy to forget and cause leaks. |
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.
Use this as one explanation rep, then continue with the Angular interview questions cluster or a guided prep path.