Architecting RxJS shareReplay: Requirements, Minimal Design, Trust Boundaries, and Operational Checks
A concise guide to using shareReplay for multicasting with replay buffer, covering when it fits, how to set it up safely, and what to watch for in production.
06 Apr 2026, 08:46 UTC

Requirements
\nBefore applying shareReplay you need to answer three questions:
- \n
- What work should be shared? Identify the upstream Observable that performs expensive or side‑effect‑free computation (e.g., a HTTP request, a heavy calculation, or a timer). \n
- How much history must new subscribers see? Decide whether you need a fixed number of recent values (
bufferSize) or a time window (windowTime). \n - Should the upstream subscription stay alive when there are no listeners? If you want automatic teardown, enable
refCount; otherwise keep the subscription alive until manual unsubscription. \n
Answering these clarifies the contract you are establishing with downstream consumers.
\n\nSmallest Suitable Design
\nThe minimal implementation consists of three pieces:
\nimport { of, shareReplay } from 'rxjs';\nimport { delay } from 'rxjs/operators';\n\n// Example source: emits 1,2,3 with 10 ms spacing\nconst source$ = of(1, 2, 3).pipe(delay(10));\n\n// shareReplay with a buffer of 2 values and automatic refCount\nconst shared$ = source$.pipe(\n shareReplay({ bufferSize: 2, refCount: true })\n);\n\n// First subscriber (starts immediately)\nshared$.subscribe({\n next: v => console.log('sub1:', v),\n complete: () => console.log('sub1 complete')\n});\n\n// Second subscriber (starts after 20 ms)\nsetTimeout(() => {\n shared$.subscribe({\n next: v => console.log('sub2:', v),\n complete: () => console.log('sub2 complete')\n });\n}, 20);\n\nRun the snippet in a Node.js environment with RxJS v7+ installed (npm i rxjs) or in a browser console that has RxJS loaded via a CDN. No special permissions are required.
What happens:
\n- \n
- The source subscription is created when the first subscriber arrives. \n
- Values 1, 2, 3 are emitted; the internal buffer holds the last two (2 and 3). \n
- When the second subscriber joins at 20 ms, it immediately receives the buffered values 2 and 3, then receives any future emissions (none in this example). \n
- When both subscribers have unsubscribed,
refCount: truecauses the source subscription to be torn down. \n
Trust/Data Boundaries
\nshareReplay creates a trust boundary between the upstream source and downstream subscribers:
- \n
- Upstream unaware of downstream count. The source Observable is subscribed to exactly once (while refCount keeps it alive). It does not receive notifications about how many observers are attached. \n
- Downstream receives deterministic replay. Every new subscriber gets the same buffered emissions in the exact order they were emitted, regardless of when they subscribe. \n
- Side‑effect isolation. If you place pure operators (e.g.,
map,filter) upstream ofshareReplay, their computations are executed once and the results are shared. Operators that cause side effects (tapperforming API calls,domutating state) must be placed after the replay, otherwise those side effects would be re‑executed for each new subscriber. \n
Operational Checks
\nTo verify that the operator behaves as expected in a running system, perform the following checks:
\n- \n
- Subscription count monitoring. When using
refCount: true, log therefCountvalue (RxJS exposes it via the internalSubscribercount, but you can approximate by wrapping the source in atapthat increments/decrements a counter). Ensure the count goes to zero when the last subscriber leaves and rises again on a new subscription. \n - Buffer size validation. Subscribe late and assert that you receive exactly the configured number of past values (or those within the time window). For a time‑based window, use virtual time testing (e.g., Jest with
jest.useFakeTimers()) to advance time and confirm old values drop out. \n - Error propagation. Replace the source with
throwError(() => new Error('boom'))and subscribe before and after the error. Both subscriptions should receive the error notification; no successful values should be emitted after the error. \n - Completion handling. If the source completes, ensure that subsequent subscribers receive the completion notification immediately (after any buffered values). \n
These checks can be automated in unit tests using RxJS testing utilities (TestScheduler) or manually inspected in development consoles.
Failure Modes
\nUnderstanding where shareReplay can break helps you decide whether to keep the design or replace it.
- \n
- Source error. Any error emitted by the source is forwarded to all current and future subscribers. After the error, the internal subscription is considered finished; no further values will be replayed. \n
- Unbounded buffer. Calling
shareReplay()(no arguments) creates an infinite buffer that retains every emission. In long‑running streams (e.g., a websocket that never completes) this can cause a memory leak. Mitigation: specify a finitebufferSizeorwindowTime. \n - Stale subscribers after source completion. If the source completes and you rely on
refCountto restart the source on each new subscription, note that after completion the source will not emit new values; a new subscription will immediately receive the buffered values and then complete. \n - Race with concurrent hot sources. When the upstream is already a hot Observable (e.g., a subject emitting from multiple producers),
shareReplayonly guarantees order of the buffered slice; interleaving with live emissions is not guaranteed. If strict ordering of live vs. replayed values is required, consider alternative patterns such aspublishReplaywith manual connection. \n
Conditions That Would Change the Design
\nRevisit the choice of shareReplay if any of the following become true:
- \n
- Need for selective replay. If you want different subscribers to see different amounts of history (e.g., one needs the last 5 values, another only the last 1), a single
shareReplaycannot satisfy both; you would need separate replay streams or aBehaviorSubjectwith custom logic. \n - Desire to replay side effects. Should you intentionally re‑run side‑effectful work for each new subscriber (e.g., re‑fetching data on demand), place the side effect before the replay or avoid
shareReplayaltogether. \n - Back‑pressure requirements.
shareReplaydoes not apply back‑pressure to the source; if the source can produce faster than the consumer can process, consider usingbufferTimeoutorconcatAllwith a scheduler. \n - Explicit connection control. When you need fine‑grained control over when the upstream subscription starts and stops (e.g., start on a manual trigger, stop on a specific condition), replace
shareReplaywithpublishReplayand manageconnect()andunsubscribe()yourself. \n
In each case, the alternative pattern will expose the subscription lifecycle more explicitly, which may increase complexity but provides the needed flexibility.
\n\nPractical Verification Checklist
\nAfter implementing shareReplay in your codebase, run this quick checklist:
- \n
- Confirm the source subscription count behaves as expected with
refCount. \n - Validate that late subscribers receive the correct buffered values (unit test with
TestScheduleror manual timeout). \n - Ensure errors and completion are propagated to all subscribers. \n
- Check memory usage in a long‑running scenario; if using an infinite buffer, add a size or time limit. \n
- Review placement of side‑effectful operators to avoid unintentional re‑execution. \n
Following these steps gives you confidence that the multicasting design satisfies the original requirements while staying within safe operational bounds.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.