DEV Community

Alejandro
Alejandro

Posted on

Part 5: The Angular subscrition Problems That Only Appear in Production

Part 5 of the Angular in Production series

One thing I've learned about subscriptions is that they almost never become a problem during development. The application works, the data loads, the page updates correctly and everything seems perfectly fine. Then the application reaches production. Users keep the application open for hours. They navigate between pages dozend of times. They open and close dialogs. Components get destroyed and recreated over and over again. That's when subscription problems start appearing. Not because RxJS is complicated, but because the application is finally behaving the way real users use it.


The First Subscription Isn't the Problem

When you're learning Angular, subscribing to an Observable feels completely natural.

ngOnInit() {
    this.userService.getUsers().subscribe(users=> {
        this.users = users;
    });
}
Enter fullscreen mode Exit fullscreen mode

Nothing wrong here, then another Observable appears:

ngOnInit() {
    this.userService.getUsers().subscribe(users => {
        this.users = users;
    });
    this.settingsService.getSettings().subsribe(settings => {
        this.settings = settings;
    });
}
Enter fullscreen mode Exit fullscreen mode

Then another, then route parameters, authentication and WebSockets. Eventually the component contains subscriptions everywhere. The problem isn't that there are several subscriptions. The problem is that nobody is thinking about what happens to them after the component disappears.


Production Doesn't Forgive Forgotten Subscription

One thing that suprised me early on was how difficult these bugs are to notice. Imagine a dashboard, the user enters, leaves, comes back, leaves again... Each visit creates a new subscription. If the previous ones were never cleaned up, they continue existing. Nothing crashes, nothing throws an error. The application simply starts doing more work that it should.

Sometimes you notice duplicated API requests. Others event handlers execute multiple times and others the memory usage slowly increases throughout the day. These kinds of problems are frustrating because they rarely point directly to the forgotten subscription. They usually appear somewhere completely different.


Not Every Observable Behaves the Same Way

Something that also took me a while to fully understand is that treating every Observable the same leads to unnecessary code.
For example:

this.userService.getUsers().subscribe(...);
Enter fullscreen mode Exit fullscreen mode

and

this.route.params.subscribe(...);
Enter fullscreen mode Exit fullscreen mode

Both use subscribe(). But they don't behave the same. Some Observables emit once and complete automatically. Others keep emitting for a long as they exist. Understanding that distinction changes how you think about subscriptions. I've seen components manually unsubscribing from HTTP requests that complete by themselves. I've also seen long-lived streams that were never cleaned up because they looked almost identical. The syntax is the same. The lifecycle isn't.


The Real Issue Isn't Memory Leaks

Whenever subscriptions are discussed, memory leaks usually become the main topic. They're important, but I actually think they're only one consequence of a larger problem. The real issue is losing control over who owns the data flow. Imagine a component listening to three different streams. One updates every second. Another reacts to route changes and another listens for authentication updates.

Now imagine trying to understand why a value suddenly changes. The longer I work with Angular, the less I think about subscriptions as "things to unsubscribe from". Instead, I think about them as relationships that need clear ownership. If it's not obvious who owns a subscription and when it should stop existing, that's usually a sign that the component is becoming harder to reason about.


Manual Subscription Management Doesn't Scale

For a long time, this was the pattern I used everywhere:

private subscriptions = new Subscription();
ngOnInit() {
    this.subscriptions.add(
        this.userService.getUsers().subscribe(users => {
            this.users=users;
        })
    );
    this.subscriptions.add(
        this.route.params.subscribe(params => {
            this.id = params['id];
        })
    );
}

ngOnDestroy() {
    this.subscriptions.unsubsribe();
}
Enter fullscreen mode Exit fullscreen mode

There's nothing technically wrong with it. In fact, plenty of Angular applications still use this approach successfully. The problem is that it doesn't scale very well. Every new subscription means remembering to add it. Every refactor means checking whether it's still being cleaned up correctly. Eventually, subscription management starts becoming another responsibility inside the component. And just like we discussed in the previous article, components tend to become difficult to maintain when they slowly acumulate responsibilities that aren't directly related to rendering the UI.


Let the Template Handle Subscriptions Whenever Possible

One change that had a bigger impact than I expected was simply moving subscription out of the component whenever I didn't actually need to manage them there. For a long time, I would write something like this:

users: User[] = [];
ngOnInit() {
    this.userService.getUsers().subscribe(users=> {
        this.users = users;
    });
}
Enter fullscreen mode Exit fullscreen mode

Today, if the component only needs to display the data, I usually expose the Observable directly:

users$ = this.userService.getUsers();
Enter fullscreen mode Exit fullscreen mode

And lt the template subscribe:

<ul *ngIf="users$ | async as users">
    <li *ngFor="let user of users">
        {{ user.name }}
    </li>
</ul>
Enter fullscreen mode Exit fullscreen mode

Besides making the component smaller, it also removes an entire category of problems. There's no manual subscription. No cleanup code. No chance of forgetting to unsubscribe. It's a surprisingly small change that often makes component much easier to reason about.


Some Streams Deserve Their Own Lifecycle

Of course, not everything can be handled with the async pipe. Sometimes a component genuinely needs to react to a stream. Route parameters. WebSockets. Form value changes. Authentication state. Those situations are perfectly valid. The important part is making the life cycle obvious.

Instead of having subscription scattered across different methods, I try to keep them grounded together so it's immediately clear what the component is listening to. That doesn't just help me. It also helps the next developer who opens the file months later.

If someone has to search through 600 lines of code to discover where a subscription starts, chances are the component has already become too difficult to maintain.


Modern Angular Makes This Much Easier

Angular has gradually improved the developer experience around subscrptions. One example is takeUntilDestroyed(), which removes a lot of boilerplate while keeping the lifecycle explicit. Instead of manually creating a Subscription object and remembering to clean everything up, the intent becomes much clearer:

import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; 
constructor(
    private route: ActivatedRoute,
    private destroyRef: DestroyRef
) {}

ngOnInit(){
    this.route.params
        .pipe(takeUntilDestroyed(this.destroyRef))
        .subscribe(params => {
            this.id = params['id'];
        });
}
Enter fullscreen mode Exit fullscreen mode

The biggest advantage isn't that it saves a few lines of code. It's that subscription management becomes much harder to forget. The less manual lifecycle code I have to maintain, the fewer mistakes I tend to make.


Too Many Subscriptions Usually Point to Another Problem

One pattern I've started noticing is that components with lots of subscriptions often have another issue hiding underneath. They're coordinating too many independent pieces of the application. For example, if one component subscribes to:

  • route parameters
  • authentication
  • user preferences
  • notifications
  • WebSocket updates
  • feature flags
  • multiple API calls

the subscriptions themselves probably aren't the real problem. The component has simply become responsible for too much. In those situations, I rarely start by looking at RxJS. I start by asking whether some those responsibilities belong somewhere else.

Sometimes moving logi into a service removes several subscriptions entirely. Others extracting a child component naturally reduces the amount of reactive code. Fixing the architecture often fixes the subscription problems as well.


Conclusion

Most subscription issues don't appear because developers don't understand RxJS. They appear because applications evolve. A component that originally subscribed to one Observable slowly starts listening to five or six different streams.

Every new subscription makes sense on its own. Together, they become increasingly difficult to manage. Today, I don't try to eliminate subscriptions. I try to make their lifecycle obvious. If the template can own the subscription, I let it. If the component needs it, I make sure its lifetime is explicit. And ig I find myself adding more and more subscriptions to the same class, I usually stop and ask a different question: Is this component doing more work that it should?

Quite often, the answer has nothing to do with RxJS. It's an architectural problem that subscriptions simply happened to expose.


Next Article in This Series

Part 6: When Your Angular Frontend and API Stop Agreeing

Previous Articles in This Series

Part 1: Why Angular Applications Get Slower as They Grow
Part 2: Angular Bundle Size Is Only One Part of Performance
Part 3: The Angular Change Detection Mistakes That Make Large Apps Feel Slow
Part 4: When an Angular Component Becomes Too Large to Maintain




Thanks for reading! If your Angular application has accumulated performance or maintainability problems over time, this is the kind of work I help teams with: debugging existing codebases, improving performance and refactoring incrementally. You have a link to my upWork in my Dev profile.

Top comments (0)