Multicasting HTTP Requests with RxJS shareReplay: A Practical Configuration Guide
Configure RxJS shareReplay to multicast cold HTTP observables across multiple subscribers with a single source execution, automatic cleanup via refCount, and proper error handling.
15 May 2026, 08:35 UTC

The Problem: Duplicate HTTP Requests from Multiple Subscribers
When multiple components subscribe to the same cold observable — such as an Angular HttpClient call — each subscription triggers a separate network request. This wastes bandwidth, increases latency, and can cause race conditions when the backend isn't idempotent. The fix is to multicast the observable so the source executes once and the result is shared.
Takeaway: Use shareReplay({ bufferSize: 1, refCount: true }) on the cold observable. This replays the most recent emission to late subscribers, keeps the source subscribed only while at least one consumer is listening, and avoids manual Subject plumbing.
Prerequisites
- RxJS 7.x or later (the
refCountoption was stabilized in v7). - A cold observable source — typically an HTTP call, a one-time database fetch, or any observable that starts work on subscription.
- TypeScript 4.7+ for strict typing of the operator config (optional but recommended).
Minimal Working Configuration
import { shareReplay } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
interface User { id: number; name: string; }
function fetchUser(http: HttpClient, id: number): Observable {
return http.get(`/api/users/${id}`).pipe(
shareReplay({ bufferSize: 1, refCount: true })
);
}
With this pipe, the first subscriber triggers the HTTP request. Subsequent subscribers within the same refCount window receive the cached response instantly. When the last subscriber unsubscribes, the internal ReplaySubject disconnects from the source; a new subscription later will re-execute the HTTP call.
Why These Options?
| Option | Value | Reason |
|---|---|---|
bufferSize | 1 | Replay only the latest value. An HTTP response is a single emission; caching more wastes memory. |
refCount | true | Auto-unsubscribe from the source when the subscriber count drops to zero. Prevents the source from staying alive indefinitely (memory leak) and allows fresh data on the next subscription burst. |
Omitting refCount (or setting it to false) keeps the source subscribed forever after the first subscriber — useful for truly singleton data like configuration, but dangerous for user-specific requests.
Common Pitfall: Error Propagation Without Recovery
If the source errors, shareReplay forwards that error to all current and future subscribers until the refCount drops to zero and a new subscription restarts the source. For HTTP calls you usually want a single retry or a fallback, not a permanent error cache.
import { catchError, of } from 'rxjs';
function fetchUserSafe(http: HttpClient, id: number): Observable {
return http.get(`/api/users/${id}`).pipe(
catchError(err => {
console.error('User fetch failed', err);
return of(null); // graceful degradation
}),
shareReplay({ bufferSize: 1, refCount: true })
);
}
Place catchError before shareReplay so the fallback value (or empty) is what gets replayed. If you put it after, the error still gets cached and replayed.
Diagnostic Check: Verify Single Execution
Run this in a test or a component ngOnInit to confirm the source runs once:
import { of, delay, tap } from 'rxjs';
const source$ = of('user-data').pipe(
delay(100),
tap(() => console.log('SOURCE EXECUTED')),
shareReplay({ bufferSize: 1, refCount: true })
);
source$.subscribe(v => console.log('Sub A:', v));
setTimeout(() => source$.subscribe(v => console.log('Sub B:', v)), 50);
// Expected console:
// SOURCE EXECUTED
// Sub A: user-data
// Sub B: user-data
// (only ONE "SOURCE EXECUTED")
If you see "SOURCE EXECUTED" twice, either refCount dropped to zero between subscriptions (check unsubscribe timing) or the operator isn't applied to the same observable instance.
RefCount Burst Edge Case
When many subscribers attach and detach rapidly (e.g., a list component that destroys and recreates items on each keystroke), refCount: true can cause the source to re-subscribe repeatedly. Each transition from 0 → 1 subscribers re-executes the HTTP call.
Mitigation options:
- Add a small
windowTimevia a customrefCountfactory (RxJS 7.4+):import { refCount, timer } from 'rxjs'; shareReplay({ bufferSize: 1, refCount: () => refCount({ connector: () => new ReplaySubject(1), resetOnError: false, resetOnComplete: false, resetOnRefCountZero: () => timer(500) // wait 500ms before disconnecting }) }) - Move the
shareReplayhigher in the component tree so the observable lives longer (e.g., in a service singleton). - If the data is truly static for the session, drop
refCountand accept the permanent subscription.
Verification Checklist
- Single source execution: Add a side-effect log or spy on the HTTP service; confirm one call per
refCountcycle. - Late subscriber receives value: Subscribe 200 ms after the first; assert it gets the same data without delay.
- Cleanup on unsubscribe: Unsubscribe all; wait for any
timerif customrefCountused; resubscribe and confirm source re-executes. - Error handling: Force a 500 response; verify all active subscribers get the error/fallback and that a later subscription retries (or stays failed if
catchErroris aftershareReplay).
When Not to Use shareReplay
- Hot sources (WebSocket, DOM events,
interval): The source emits independently of subscriptions.shareReplaywill replay the last value but won't prevent the source from running; you may get duplicate emissions or stale replays. - Infinite streams where you need every value:
bufferSize: 1drops history. Use a larger buffer or a plainmulticast(() => new ReplaySubject(N))if you need replay of N values. - Memory-sensitive long-running apps with high-frequency emissions: Even
bufferSize: 1holds one value per shared observable. If you create thousands of distinct shared observables (e.g., per list item), consider a centralized cache instead.
Rollback / Recovery
If you discover the shared observable is caching stale data after a user action (e.g., profile update), you have two options:
- Invalidate manually: Keep a reference to the
Observablereturned by the service method and replace it with a fresh one after the mutation:// In a service private userCache$ = fetchUser(http, id); getUser() { return this.userCache$; } refreshUser() { this.userCache$ = fetchUser(http, id); } - Use a subject trigger: Expose a
BehaviorSubjectthat the service calls.next()on after a write; the shared observable canswitchMapto that trigger.
Both approaches avoid the complexity of resetOnRefCountZero timing and make cache invalidation explicit.
Summary
Apply shareReplay({ bufferSize: 1, refCount: true }) directly to cold HTTP observables to eliminate duplicate requests. Place catchError before it for graceful degradation. Verify with a side-effect log that the source executes once per subscriber group. For bursty subscription patterns, either extend the refCount disconnect delay or hoist the shared observable to a longer-lived scope. This pattern replaces manual ReplaySubject + multicast boilerplate and is the supported RxJS 7+ idiom for request deduplication.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.