RxJS shareReplay: Multicast, Cache, and refCount Explained
Learn how RxJS’s shareReplay operator turns a cold Observable into a multicast source, caches emissions, and why refCount:true matters. See a concrete example, version‑specific notes, and common mistakes to avoid.
02 Jan 2026, 12:08 UTC

What shareReplay Actually Does
shareReplay turns a cold Observable into a multicast source that stores the last bufferSize values. New subscribers receive those cached values immediately, without re‑executing the source logic. The operator is especially useful for expensive HTTP calls, shared websockets, or any operation that should run only once and then be reused.
Key Points at a Glance
- Multicasts the source Observable.
- Caches the most recent emissions up to
bufferSize. - Can automatically unsubscribe when the last subscriber drops via
refCount:true. - RxJS 7 requires an options object; older code that passes a number may misbehave.
How It Works Under the Hood
Internally, shareReplay creates a ConnectableObservable that subscribes to the source only once. The emissions are buffered in a simple array. When a new subscriber joins, the buffered values are replayed before any new values arrive.
In RxJS 7 the signature is:
shareReplay({ bufferSize?: number, refCount?: boolean })
When refCount:true is set, the underlying subscription to the source is automatically closed when the subscriber count drops to zero. If omitted, the source stays connected forever, which can keep side‑effects running and consume resources.
Concrete Example: Caching a Delayed HTTP Call
Below is a minimal TypeScript snippet that demonstrates the typical pattern. Replace fetchData() with your actual HTTP call.
import { of, timer } from 'rxjs';
import { shareReplay, tap } from 'rxjs/operators';
// Simulate an expensive operation that emits once after 1s
const source$ = timer(1000).pipe(
tap(() => console.log('Source executed')),
tap(value => console.log('Source value:', value))
);
// Apply shareReplay with bufferSize 1 and automatic refCount
const shared$ = source$.pipe(
shareReplay({ bufferSize: 1, refCount: true })
);
// First subscriber – triggers the source
shared$.subscribe({
next: v => console.log('Subscriber 1:', v),
complete: () => console.log('Subscriber 1 complete')
});
// Second subscriber after 2s – should receive cached value instantly
setTimeout(() => {
shared$.subscribe({
next: v => console.log('Subscriber 2:', v),
complete: () => console.log('Subscriber 2 complete')
});
}, 2000);
Expected runtime behaviour:
- At ~1s: "Source executed" and "Source value: 0" appear once.
- Subscriber 1 receives the value immediately after the source emits.
- At ~2s: Subscriber 2 subscribes and receives the cached value instantly (no new "Source executed" log).
- After both subscribers unsubscribe, the underlying source unsubscribes automatically because
refCount:true.
Version‑Specific Considerations
| RxJS Version | shareReplay Signature | Common Pitfall |
|---|---|---|
| 6.x | shareReplay(number) | Single number interpreted as bufferSize; refCount not available. |
| 7.x+ | shareReplay({bufferSize, refCount}) | Passing a number triggers a deprecation warning and may be interpreted as bufferSize only. |
If you upgrade from RxJS 6 to 7, review any shareReplay usages. Old patterns like shareReplay(1) still work but you lose the ability to set refCount:true unless you refactor to the new options object.
Common Mistakes and How to Avoid Them
- Forgetting
refCount:true: The source stays subscribed even after all observers leave. This can keep timers running or open sockets, leading to memory leaks.- Fix:
shareReplay({ bufferSize: 1, refCount: true })
- Fix:
- Using shareReplay on an infinite source without a buffer: If you omit
bufferSizeor set it to a large number, every emission will be stored. Over time, the buffer grows unbounded, exhausting memory.- Fix: Specify a finite
bufferSizeor usetaketo limit emissions.
- Fix: Specify a finite
- Applying shareReplay to a hot Observable (e.g.,
interval): The source continues to emit regardless of new subscribers. Replay will replay the same buffered values to each new subscriber, which may not be what you want.- Fix: Use
shareReplayonly on cold sources or combine withtakeUntilto bound the stream.
- Fix: Use
- Assuming shareReplay caches all past values by default: The default
bufferSizeis 1 in RxJS 7. If you need a larger history, setbufferSizeexplicitly.
Practical Verification Checklist
- Run the example and confirm the source executes only once.
- Add a console.log inside the source’s
tapto watch for repeated executions after all subscribers have unsubscribed. - Use
finalizeon the source to see when it completes:tap(...).pipe(finalize(() => console.log('Source finalized'))). - When testing with an infinite source, set
bufferSize: 1and observe that the buffer does not grow beyond this size. - Check the subscription count by subscribing to
shared$.pipe(shareReplay(...))and inspecting the returned Subscription’sclosedproperty after unsubscription.
When to Use shareReplay
- Shared HTTP requests that should be performed once per component lifecycle.
- WebSocket streams that you want to replay the last message to late subscribers.
- Any expensive synchronous or asynchronous operation that benefits from caching the result.
When Not to Use shareReplay
- Streams that must always reflect real‑time data, e.g., live market feeds.
- Infinite streams without a defined buffer size or completion condition.
- Hot Observables where the source logic is already running and you don’t want to interfere.
Conclusion
shareReplay is a powerful tool for turning cold Observables into reusable, multicast sources with minimal boilerplate. The crux is to always consider bufferSize and refCount:true to avoid hidden resource consumption. Keep an eye on the RxJS version you’re using, as the operator’s signature changed in v7. With the guidelines above, you can confidently apply shareReplay in production code while steering clear of the most common pitfalls.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.