Using RxJS shareReplay as a Simple HTTP Cache in Angular Services
Learn how to apply shareReplay to avoid duplicate HTTP requests, set trust boundaries, check operation, and recognize when the design must evolve.
12 Jan 2026, 10:08 UTC

Requirements
\nIn an Angular application, a service method that wraps HttpClient.get often needs to:
- \n
- Prevent duplicate network calls when multiple components subscribe at the same time. \n
- Automatically clean up the underlying subscription when no consumers remain, to avoid dangling requests and memory leaks. \n
- Propagate errors to each subscriber unless a fallback is explicitly provided. \n
- Allow optional manual control of the cached value’s lifetime (e.g., stale‑while‑revalidate). \n
Smallest Suitable Design
\nThe minimal RxJS construction that satisfies the above is:
\nimport { HttpClient } from '@angular/common/http';\nimport { shareReplay } from 'rxjs/operators';\n\n@Injectable({ providedIn: 'root' })\nexport class DataService {\n constructor(private http: HttpClient) {}\n\n fetchData() {\n return this.http.get('/api/data').pipe(\n shareReplay(1) // bufferSize = 1, refCount defaults to true\n );\n }\n}\n\nshareReplay(1) creates a hot observable that:
- \n
- Subscribes to the cold
http.getsource on the first subscriber. \n - Buffers the latest successful emission and replays it to any later subscriber. \n
- When the subscriber count drops to zero, the internal subscription is automatically disposed because
refCountdefaults totrue. \n
Trust / Data Boundaries
\nThe trust boundary lies between the cold source (HttpClient.get) and the hot observable produced by shareReplay:
- \n
- Cold side: Each call to
getcreates a new XHR/fetch; no side‑effects occur until subscription. \n - Hot side: After the first subscription, all downstream consumers share the same underlying request; they cannot affect the source directly. \n
Thus, the service guarantees that at most one HTTP request is in flight for a given call to fetchData(), regardless of how many components subscribe.
Operational Checks
\nTo verify the design works as intended:
\n- \n
- Open the browser’s Network tab. \n
- In a component, subscribe twice in quick succession: \n
this.dataService.fetchData().subscribe();\nthis.dataService.fetchData().subscribe();\n\n- \n
- Confirm that only a single request to
/api/dataappears. \n - After both subscriptions are unsubscribed (e.g., via
ngOnDestroyor manualunsubscribe()), wait a short interval and ensure no further requests are triggered. \n - Optionally, inspect the returned observable’s internal state by adding a
tapoperator that logs when the source subscription is created and destroyed. \n
These checks confirm that the trust boundary is respected and that resource cleanup occurs.
\nFailure Modes
\n- \n
- Incorrect refCount: If you explicitly set
shareReplay(1, false)orshareReplay(1, undefined, false), the internal subscription remains alive after all subscribers leave, causing persistent HTTP requests and a memory leak. \n - Error propagation: By default,
shareReplaydoes not cache error notifications. A failed request will be re‑executed for each new subscriber unless you handle errors upstream, e.g.,catchError(() => of(fallbackData)). \n - Scheduler mismatches (RxJS < 7): Versions prior to 7 lack a scheduler argument; using
shareReplaywith async operations that depend on a specific scheduler may yield unexpected timing (e.g., delayed completions). \n - Stale data: The buffer holds only the latest successful value. If the source observable completes (as HTTP does), late subscribers receive that value forever, even if the backend data has changed. \n
Conditions That Would Change the Design
\nRevisit the design when any of the following arise:
\n- \n
- Need to cache multiple distinct values (e.g., paginated lists). Increase
bufferSizeor switch to a dedicated caching operator likepublishReplaywith manual ref‑counting. \n - Require a stale‑while‑revalidate strategy: keep the cached value while issuing a background refresh. Combine
shareReplaywithexpandorrepeatand a timer. \n - Want to control the lifetime manually (e.g., logout clears cache). Set
refCount: falseand manage the subscription via aSubjectortakeUntil. \n - Must cache errors as well (e.g., show a persistent error message). Apply
catchErrorbeforeshareReplayto transform errors into a successful emission. \n - Operating in an environment where RxJS version is below 7 and scheduler sensitivity matters; upgrade or explicitly pass a scheduler to
shareReplayif available. \n
When any of these conditions appear, the smallest design (shareReplay(1)) is no longer sufficient, and a more elaborate caching architecture should be considered.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.