| |

Angular 45 🅰️ RxJS Multicasting and Sharing

Every Observable in RxJS is either cold or hot, and the distinction determines what happens when more than one subscriber arrives. A cold Observable creates a new producer for each subscriber — two subscribers to an HTTP call send two requests, and two subscribers to an interval create two timers. A hot Observable shares a single producer among all subscribers — one request, one timer, one source of values. The operators that convert a cold Observable into a hot one are the multicasting operators: share, shareReplay, publish, and their relatives. They are the mechanism that prevents duplicate work, shares a stream among many consumers, and caches a value so late subscribers receive it. This chapter covers the cold/hot distinction, the multicasting operators, the difference between share and shareReplay, the reference-counting behavior that resets the cache, and the patterns that make sharing safe. It continues the RxJS sequence from Angular 39 through 44 and treats sharing as the performance and correctness concern it is.

Key point: A cold Observable runs its producer for each subscriber; a hot Observable shares one producer among all subscribers. share() is the simplest multicasting operator — it subscribes to the source when the first subscriber arrives and unsubscribes when the last one leaves. shareReplay({ bufferSize: 1, refCount: true }) is the same plus a replay of the last value to late subscribers. The refCount option determines whether the shared subscription resets when all subscribers leave. publish and multicast are the lower-level operators that give explicit control over the shared Subject. In Angular, the most common use is sharing an HTTP Observable among several consumers — a component and a template, or two components that need the same data.


Why sharing matters

An Observable is a description of a stream. When two subscribers subscribe to the same cold Observable, the description is executed twice, and two independent streams are produced. This is correct when the streams are independent, and wrong when the work should be done once.

The duplicate request problem. A component subscribes to an HTTP Observable in ngOnInit, and the template subscribes to the same Observable with the async pipe. Without sharing, two requests are sent. The second is a duplicate, and the server sees two identical requests for one view.

The duplicate timer problem. Two components subscribe to an interval Observable. Without sharing, two timers run, and the values arrive at slightly different times. The two components are out of sync, and the resources are doubled.

The cost of duplication. A duplicate HTTP request costs bandwidth, server capacity, and latency. A duplicate timer costs a background task. A duplicate WebSocket connection costs a socket. In a large application, the duplication compounds, and the fix is to share the source.

Why sharing is not the default. Cold Observables are the default because they are the simpler mental model. Each subscriber gets its own stream, and there is no shared state to reason about. Sharing introduces the question of when the shared subscription starts and stops, which is the refCount decision. The default is cold, and sharing is opt-in.

Why the distinction is subtle. The type signature of an Observable does not say whether it is cold or hot. of(1, 2, 3) is cold. fromEvent(document, 'click') is hot. interval(1000) is cold. Subject is hot. The distinction is in the producer’s behavior, not in the type, which is why it must be known rather than inferred.

Why the multicasting operators exist. A cold Observable is a single-subscriber description. To share it, a Subject is placed in the middle: the source subscribes to the Subject, and the Subject multicasts to all subscribers. The multicasting operators are the machinery that connects the source to the Subject and manages the subscription lifecycle. share and shareReplay are the high-level forms; publish and multicast are the lower-level ones.


share — the simplest multicast

share returns a new Observable that subscribes to the source when the first subscriber arrives and unsubscribes from the source when the last subscriber leaves.

import { interval } from 'rxjs';
import { share, take } from 'rxjs/operators';

const shared$ = interval(1000).pipe(share());

shared$.subscribe((v) => console.log('A:', v));
shared$.subscribe((v) => console.log('B:', v));

The two subscribers share a single timer. Each tick is emitted to both, and the values are synchronized. Without share, two timers would run.

Why the first subscriber starts the source. The shared Observable is dormant until the first subscriber arrives. When the first subscribes, the source is subscribed, and the values begin to flow. When the second subscribes, the source is already running, and the second subscriber receives the values from the moment it subscribes.

Why the source stops when the last subscriber leaves. When the last subscriber unsubscribes, the share operator unsubscribes from the source. This is the refCount behavior — the reference count of subscribers drops to zero, and the source is released. This is the default in modern RxJS.

Why a late subscriber misses earlier values. share does not replay. A subscriber that arrives after a value has been emitted misses it. The stream is shared, but the history is not.

Why share is used for events. A shared event stream — mouse movements, keystrokes, WebSocket messages — is a natural fit. The source is expensive or unique, and every subscriber should receive the same events from the moment they subscribe. The absence of replay is correct because the events are transient.

Why share is not enough for HTTP. An HTTP Observable emits one value and completes. If the first subscriber receives the value and completes, and then a second subscriber arrives, the source has already completed, and the second subscriber receives nothing. For HTTP, shareReplay is needed to cache the value for late subscribers.


shareReplay — share and cache

shareReplay is share plus a replay of the last bufferSize values to new subscribers. It is the operator for caching a value so that late subscribers receive it.

const users$ = this.http.get<User[]>('/api/users').pipe(
  shareReplay({ bufferSize: 1, refCount: true }),
);

The first subscriber sends the request. When the response arrives, the value is emitted to the first subscriber and cached. A second subscriber that arrives after the response receives the cached value immediately, without a second request.

Why shareReplay is the HTTP pattern. The request should be sent once, and the result should be available to every consumer. The shareReplay with bufferSize: 1 caches the single value, and the second subscriber gets it from the cache.

The refCount option. The refCount: true option makes the shared subscription reset when all subscribers leave. When the count drops to zero, the source is unsubscribed and the cache is cleared. The next subscriber starts a fresh request.

shareReplay({ bufferSize: 1, refCount: true })

Without refCount, the shared subscription persists after the last subscriber leaves, and the cache is never cleared. The source is never unsubscribed, and a later subscriber receives the stale cached value.

Why refCount is the modern default. In older versions of RxJS, shareReplay(1) had no refCount and the behavior was the source of subtle bugs — a component that unsubscribed and resubscribed received a stale value. The refCount: true option makes the behavior predictable: the shared subscription lives exactly as long as there is a subscriber.

Why the buffer size matters. bufferSize: 1 caches the last value, which is what most state needs. A larger buffer caches more history, which is rarely needed and costs memory. For HTTP, 1 is correct.

The difference between share and shareReplay.

AspectshareshareReplay({ bufferSize: 1 })
ReplaysNoLast value
Late subscriberMisses valuesReceives cached value
UseEventsHTTP, state

Why a late subscriber after completion receives the value. shareReplay caches the value, and a subscriber that arrives after the source has completed still receives the cached value and the completion. This is what makes it correct for HTTP — the request is sent once, and every subscriber receives the result, whether it subscribes before or after the response.

Why the refCount decision is the important one. With refCount: true, the cache lives as long as there is a subscriber, and a new subscriber after all have left starts a fresh request. With refCount: false, the cache lives forever, and the value is never refreshed. For a component that subscribes on init and unsubscribes on destroy, refCount: true means the request is sent when the component appears and again when it reappears. For a service-level cache that should persist, refCount: false is the choice. The decision depends on whether the data should be refreshed.


The lower-level operators

share and shareReplay are the high-level operators, and they cover most cases. The lower-level operators — publish, multicast, publishReplay, publishLast, connect — give explicit control over the shared Subject and the connection.

publish and connect. publish converts a cold Observable into a ConnectableObservable using a Subject. The connect method subscribes the source to the Subject, and the subscribers receive the values.

const source$ = interval(1000).pipe(publish());
const subscription = source$.connect();  // starts the source

source$.subscribe((v) => console.log('A:', v));
source$.subscribe((v) => console.log('B:', v));

// later
subscription.unsubscribe();  // stops the source

The connect is the explicit start, and the unsubscribe is the explicit stop. This is the pattern when the source should run independently of the subscribers — the source is connected once, and subscribers come and go.

Why the explicit connect matters. The share operator connects when the first subscriber arrives and disconnects when the last leaves. The publish/connect form decouples the connection from the subscriber count, which is correct when the source should run continuously. This is the pattern for a shared service that maintains a live connection.

publishReplay. publishReplay(bufferSize) is the lower-level equivalent of shareReplay without the ref-count behavior. It publishes the source through a ReplaySubject and requires an explicit connect.

publishLast. publishLast uses an AsyncSubject, which emits only the last value on completion. It is the equivalent of shareReplay for the single-result case, and it is rarely used.

multicast with a Subject factory. The multicast operator takes a factory that returns a Subject. This is the general form, and it allows a custom Subject type.

const shared$ = source$.pipe(
  multicast(() => new Subject()),
);

The factory is called once when the source is connected, and the Subject is the shared channel. The share operator is multicast(() => new Subject()).refCount() in the modern implementation.

Why the lower-level operators are less common. The share and shareReplay operators cover the ref-count and replay cases that most code needs. The lower-level operators are for the cases where the connection should be explicit or the Subject should be custom. For Angular code, the high-level forms are the default.

Why the operators were consolidated. In older RxJS, shareReplay(1) had no refCount and the behavior was confusing. In RxJS 7, the configuration object form shareReplay({ bufferSize, refCount }) made the behavior explicit, and the older forms were deprecated. The modern forms are the ones to use.


Reference counting

Reference counting is the mechanism that ties the shared subscription’s lifetime to the number of subscribers. It is what makes the refCount option meaningful.

How the count works. When a subscriber subscribes, the count increases. When a subscriber unsubscribes, the count decreases. When the count goes from zero to one, the source is subscribed. When the count goes from one to zero, the source is unsubscribed.

Why the count resets the cache. When the source is unsubscribed, the shared state — including the cached values in a shareReplay — is released. The next subscriber starts a fresh subscription, and the source runs again. This is the behavior that prevents stale data.

Why a subscriber that arrives during the count’s transition receives the value. The transition is atomic. A subscriber that arrives while the count is above zero receives the shared values. A subscriber that arrives after the count has dropped to zero starts a new subscription and receives fresh values.

Why the count matters for a component. A component subscribes in ngOnInit and unsubscribes in ngOnDestroy. With refCount: true, the shared subscription exists only while the component is alive. If the component is destroyed and recreated, a new subscription starts, and the HTTP request is sent again. This is usually the correct behavior — the data is refetched when the component reappears.

Why the count matters for a service. A service that wants to cache the data across component lifecycles uses refCount: false, or keeps the subscription alive by other means. The cache persists, and the data is not refetched. This is the behavior for configuration or reference data that rarely changes.

Why the count should be reasoned about deliberately. The default in RxJS 7 is refCount: true for the configuration object form of shareReplay. The choice between refreshing and caching is the design decision, and the refCount option is how it is expressed.

Why the older shareReplay(1) was a footgun. Without refCount, the operator subscribed to the source on the first subscriber and never unsubscribed. The cache was never cleared, and a later subscriber received the stale value even after the source had changed. The configuration object form with refCount: true fixes this by tying the subscription to the subscriber count.


Sharing in Angular

The most common use of sharing in Angular is the HTTP Observable that is consumed by both a component and its template.

The duplicate request. A component assigns users$ = this.http.get('/api/users') and the template uses users$ | async. If the component also subscribes to users$ in its class, two requests are sent. The async pipe subscribes when the template renders, and the class subscription subscribes when the class runs.

// duplicate requests
readonly users$ = this.http.get<User[]>('/api/users');

ngOnInit() {
  this.users$.subscribe((users) => this.users.set(users));  // request 1
}
// template: {{ users$ | async }}                             // request 2

The fix is to share the Observable.

readonly users$ = this.http.get<User[]>('/api/users').pipe(
  shareReplay({ bufferSize: 1, refCount: true }),
);

The first subscription sends the request, and the second receives the cached value.

The shared service. A service that loads reference data — the current user, the available locales, the feature flags — shares the request among all consumers.

@Injectable({ providedIn: 'root' })
export class ReferenceDataService {
  private readonly http = inject(HttpClient);

  readonly countries$ = this.http.get<Country[]>('/api/countries').pipe(
    shareReplay({ bufferSize: 1, refCount: true }),
  );
}

The countries$ is a property, and the HTTP Observable is created once. Each subscriber shares the same request and the same cached value. The refCount determines whether the request is sent again after all subscribers leave.

Why the property matters. If countries$ were a method that returns the HTTP Observable, each call would create a new Observable and a new request. The property is assigned once, and the shared Observable is reused.

The shared combineLatest. A view model that combines several sources should share the result so the combination runs once.

readonly vm$ = combineLatest([this.user$, this.items$]).pipe(
  map(([user, items]) => ({ user, items })),
  shareReplay({ bufferSize: 1, refCount: true }),
);

Without sharing, each subscription to vm$ would re-run the combineLatest and the map. With sharing, the combination runs once and the result is cached.

Why the share is at the end of the pipeline. The shareReplay should be after all the transformation operators and before the consumers subscribe. The transformation runs once, and the result is shared.

Why takeUntilDestroyed interacts with sharing. The takeUntilDestroyed operator is applied per subscription, not to the shared source. A shared Observable with takeUntilDestroyed in the pipeline would complete the shared source when one component is destroyed, which is wrong. The takeUntilDestroyed belongs in the subscriber’s pipeline, not in the shared source’s.

Why sharing a combineLatest is a common performance fix. A view model that combines several sources and is subscribed to by both the class and the template re-runs the combination for each subscription. The shareReplay runs it once. The gain is small for a simple combination and significant for one with many sources or expensive projections.


Complete Example Session

import { Component, DestroyRef, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { interval, combineLatest, of } from 'rxjs';
import {
  share, shareReplay, publish, connect, map, take, takeUntilDestroyed,
  catchError, switchMap, startWith,
} from 'rxjs/operators';

// ============================================
// PART 1: SHARE — SINGLE TIMER
// ============================================

const shared$ = interval(1000).pipe(share());

shared$.subscribe((v) => console.log('A:', v));
shared$.subscribe((v) => console.log('B:', v));
// One timer, both subscribers receive each tick.

// ============================================
// PART 2: SHAREREPLAY — CACHED HTTP
// ============================================

@Component({ selector: 'app-users', standalone: true, template: `
  @for (user of users$ | async; track user.id) {
    <div>{{ user.name }}</div>
  }
` })
export class UsersComponent {
  private readonly http = inject(HttpClient);

  readonly users$ = this.http.get<User[]>('/api/users').pipe(
    shareReplay({ bufferSize: 1, refCount: true }),
  );
}

// The template subscribes once.
// A class subscription would share the same request.

// ============================================
// PART 3: SERVICE-LEVEL SHARED DATA
// ============================================

@Injectable({ providedIn: 'root' })
export class CountriesService {
  private readonly http = inject(HttpClient);

  readonly countries$ = this.http.get<Country[]>('/api/countries').pipe(
    shareReplay({ bufferSize: 1, refCount: true }),
  );
}

// ============================================
// PART 4: SHARED VIEW MODEL
// ============================================

@Component({ selector: 'app-dashboard', standalone: true, template: `` })
export class DashboardComponent {
  private readonly userService = inject(UserService);
  private readonly itemService = inject(ItemService);
  private readonly destroyRef = inject(DestroyRef);

  readonly vm$ = combineLatest([
    this.userService.user$,
    this.itemService.items$,
  ]).pipe(
    map(([user, items]) => ({ user, items })),
    shareReplay({ bufferSize: 1, refCount: true }),
  );
}

// ============================================
// PART 5: refCount TRUE VS FALSE
// ============================================

// refCount: true — resets when all subscribers leave
const refreshing$ = this.http.get('/api/data').pipe(
  shareReplay({ bufferSize: 1, refCount: true }),
);
// First subscriber: request sent.
// All leave: subscription released, cache cleared.
// New subscriber: fresh request.

// refCount: false — cache persists
const cached$ = this.http.get('/api/config').pipe(
  shareReplay({ bufferSize: 1, refCount: false }),
);
// First subscriber: request sent.
// All leave: subscription kept, cache retained.
// New subscriber: cached value.

// ============================================
// PART 6: PUBLISH AND CONNECT
// ============================================

const source$ = interval(1000).pipe(publish());
const connection = source$.connect();

source$.subscribe((v) => console.log('A:', v));
source$.subscribe((v) => console.log('B:', v));

// later
connection.unsubscribe();

// The source runs continuously.
// Subscribers come and go independently.

// ============================================
// PART 7: THE DUPLICATE REQUEST PROBLEM
// ============================================

// WITHOUT shareReplay
@Component({ selector: 'app-dup', standalone: true, template: `{{ users$ | async }}` })
export class DupComponent {
  private readonly http = inject(HttpClient);
  readonly users$ = this.http.get('/api/users');  // two subscriptions, two requests
}

// WITH shareReplay
@Component({ selector: 'app-fixed', standalone: true, template: `{{ users$ | async }}` })
export class FixedComponent {
  private readonly http = inject(HttpClient);
  readonly users$ = this.http.get('/api/users').pipe(
    shareReplay({ bufferSize: 1, refCount: true }),  // one request
  );
}

// ============================================
// PART 8: SHARE FOR EVENTS
// ============================================

const clicks$ = fromEvent(document, 'click').pipe(share());

clicks$.subscribe(() => console.log('A clicked'));
clicks$.subscribe(() => console.log('B clicked'));
// One listener, both subscribers notified.

// ============================================
// PART 9: SHARING WITH SEARCH
// ============================================

@Component({ selector: 'app-search', standalone: true, template: `` })
export class SearchComponent {
  private readonly http = inject(HttpClient);
  private readonly destroyRef = inject(DestroyRef);

  readonly results$ = this.search.valueChanges.pipe(
    debounceTime(300),
    distinctUntilChanged(),
    switchMap((term) => this.http.get(`/api/search?q=${term}`)),
    shareReplay({ bufferSize: 1, refCount: true }),
    takeUntilDestroyed(this.destroyRef),
  );
}

// ============================================
// PART 10: WHAT NOT TO DO
// ============================================

// Don't forget shareReplay on an HTTP Observable used twice
// Two requests are sent.

// Don't use shareReplay(1) without refCount
// The cache never clears.

// Don't put takeUntilDestroyed inside the shared source
// One component's destruction completes the shared source.

// Don't call a method that returns an HTTP Observable
// Each call creates a new request. Use a property.

// Don't share a stream that has side effects per subscription
// The side effects run once, which may not be intended.

// Don't use publish/connect unless the connection must be explicit
// share and shareReplay cover most cases.

The ten parts cover share, shareReplay, the service pattern, the view model, the refCount decision, publish/connect, the duplicate request problem, events, search, and the anti-patterns.


Quick Reference

Multicasting Operators

OperatorReplaysRef countUse
share()NoYesEvents, shared streams
shareReplay({ bufferSize, refCount })YesConfigurableHTTP, cached state
publish()NoNo (explicit)Explicit connection
publishReplay(n)YesNo (explicit)Cached + explicit
publishLast()Last on completeNo (explicit)Single result
multicast(factory)Depends on SubjectNo (explicit)Custom Subject

Configuration

OptionEffect
bufferSizeNumber of values to replay
refCount: trueReset when all subscribers leave
refCount: falsePersist the subscription and cache
windowTimeTime-based cache expiry

Cold vs Hot

AspectColdHot
ProducerPer subscriberShared
SubscribersIndependentShare values
Exampleof, interval, HTTPSubject, fromEvent
Sharingshare, shareReplayNative

Angular Patterns

PatternOperator
HTTP used twiceshareReplay({ bufferSize: 1, refCount: true })
Shared view modelshareReplay({ bufferSize: 1, refCount: true })
Service reference datashareReplay({ bufferSize: 1, refCount: true })
Shared event streamshare()
Live connectionpublish() + connect()

Best Practices

✅ Do This:

// Share an HTTP Observable used by multiple consumers
this.http.get('/api/users').pipe(shareReplay({ bufferSize: 1, refCount: true })) // ✅

// Use refCount: true for refreshing data
shareReplay({ bufferSize: 1, refCount: true })                 // ✅

// Use share for event streams
fromEvent(document, 'click').pipe(share())                     // ✅

// Share a view model so the combination runs once
combineLatest([...]).pipe(map(...), shareReplay({ bufferSize: 1, refCount: true })) // ✅

// Store the shared Observable as a property
readonly users$ = this.http.get('/api/users').pipe(shareReplay(...)) // ✅

// Use publish/connect when the source must run continuously
const source$ = interval(1000).pipe(publish());
const connection = source$.connect();                          // ✅

❌ Don’t Do This:

// Don't use shareReplay(1) without refCount
shareReplay(1)  // cache never clears                          // ⚠️

// Don't forget to share an HTTP Observable used twice
readonly users$ = this.http.get('/api/users');  // two requests // ⚠️

// Don't put takeUntilDestroyed inside the shared source
.pipe(shareReplay(...), takeUntilDestroyed(this.destroyRef))   // ⚠️

// Don't return a new HTTP Observable from a method
getUsers() { return this.http.get('/api/users'); }  // new each call // ⚠️

// Don't share a stream with per-subscription side effects
source$.pipe(tap(() => doSomething()), share())  // runs once      // ⚠️

// Don't use publish/connect when share works
source$.pipe(publish())  // share is simpler                     // ⚠️

// Don't use a large bufferSize without reason
shareReplay({ bufferSize: 100 })  // memory cost                // ⚠️

Common Pitfalls

PitfallProblemSolution
No share on HTTPDuplicate requestsshareReplay
shareReplay(1) no refCountStale cacherefCount: true
takeUntilDestroyed in shared sourceShared source completesPlace in subscriber
Method returns HTTPNew Observable per callUse a property
share without replayLate subscriber missesshareReplay
Large bufferMemory costbufferSize: 1
publish without connectSource not startedCall connect
Shared stream with side effectsSide effects run onceMove the side effect

Real-World Examples

1. Shared HTTP request

readonly users$ = this.http.get('/api/users').pipe(
  shareReplay({ bufferSize: 1, refCount: true }),
);

2. Service reference data

readonly countries$ = this.http.get('/api/countries').pipe(
  shareReplay({ bufferSize: 1, refCount: true }),
);

3. Shared view model

readonly vm$ = combineLatest([user$, items$]).pipe(
  map(([user, items]) => ({ user, items })),
  shareReplay({ bufferSize: 1, refCount: true }),
);

4. Shared event stream

const clicks$ = fromEvent(document, 'click').pipe(share());

5. Cached config that persists

readonly config$ = this.http.get('/api/config').pipe(
  shareReplay({ bufferSize: 1, refCount: false }),
);

6. Live connection

const source$ = interval(1000).pipe(publish());
const connection = source$.connect();

7. Shared search results

readonly results$ = this.search.valueChanges.pipe(
  switchMap((term) => this.http.get(`/api/search?q=${term}`)),
  shareReplay({ bufferSize: 1, refCount: true }),
);

8. Shared interval

const tick$ = interval(1000).pipe(share());

9. WebSocket stream

const messages$ = fromWebSocket(url).pipe(share());

10. Shared combined data

readonly data$ = forkJoin({ user: userReq, items: itemsReq }).pipe(
  shareReplay({ bufferSize: 1, refCount: true }),
);

Visual: Cold vs Shared

┌──────────────────────────────────────────────────────────┐
│  COLD (no sharing)                                       │
│                                                          │
│  source$ ──► subscriber A ──► producer A                 │
│  source$ ──► subscriber B ──► producer B                 │
│                                                          │
│  Two producers. Two requests. Independent streams.       │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  SHARED                                                   │
│                                                          │
│  source$ ──► share ──┬──► subscriber A                   │
│                      │                                   │
│                      └──► subscriber B                   │
│         │                                                │
│         └──► one producer                                │
│                                                          │
│  One producer. One request. Values shared.               │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: share vs shareReplay

┌──────────────────────────────────────────────────────────┐
│  share()                                                 │
│                                                          │
│  Source emits:  ──v1──v2──v3──►                          │
│                                                          │
│  Subscriber A (from start):  v1, v2, v3                  │
│  Subscriber B (after v2):    v3                          │
│  B misses v1 and v2.                                     │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  shareReplay({ bufferSize: 1 })                          │
│                                                          │
│  Source emits:  ──v1──v2──v3──►                          │
│                                                          │
│  Subscriber A (from start):  v1, v2, v3                  │
│  Subscriber B (after v3):    v3 (from cache)             │
│  B receives the cached v3 immediately.                   │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: refCount

┌──────────────────────────────────────────────────────────┐
│  refCount: true                                          │
│                                                          │
│  Sub A subscribes ──► count = 1 ──► source starts        │
│  Sub B subscribes ──► count = 2                          │
│  Sub A leaves     ──► count = 1                          │
│  Sub B leaves     ──► count = 0 ──► source stops         │
│                                                          │
│  New subscriber   ──► count = 1 ──► source starts again  │
│  Cache cleared. Fresh request.                           │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  refCount: false                                         │
│                                                          │
│  Sub A subscribes ──► count = 1 ──► source starts        │
│  Sub A leaves     ──► count = 0 ──► source keeps running │
│                                                          │
│  New subscriber   ──► cached value                       │
│  Cache persists. No new request.                         │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Duplicate Request Problem

┌──────────────────────────────────────────────────────────┐
│  WITHOUT sharing                                         │
│                                                          │
│  Component ngOnInit ──► subscribe ──► HTTP request 1     │
│  Template async pipe ──► subscribe ──► HTTP request 2    │
│                                                          │
│  Two requests. The server sees two identical calls.      │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  WITH shareReplay                                        │
│                                                          │
│  Component ngOnInit ──► subscribe ──► HTTP request       │
│  Template async pipe ──► subscribe ──► cached value      │
│                                                          │
│  One request. Both consumers receive the result.         │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: publish and connect

┌──────────────────────────────────────────────────────────┐
│  const source$ = interval(1000).pipe(publish())          │
│                                                          │
│  ┌──────────────┐                                        │
│  │  source      │  (cold, not started)                   │
│  └──────────────┘                                        │
│                                                          │
│  const connection = source$.connect()                    │
│       │                                                  │
│       └── the source starts running                      │
│                                                          │
│  source$.subscribe(A)  ──► A receives ticks              │
│  source$.subscribe(B)  ──► B receives ticks              │
│                                                          │
│  connection.unsubscribe()                                │
│       │                                                  │
│       └── the source stops                               │
│                                                          │
│  The connection is explicit, not tied to subscriber count│
│                                                          │
└──────────────────────────────────────────────────────────┘

Summary

OperatorReplaysRef countUse
share()NoYesEvents
shareReplay({ bufferSize: 1, refCount: true })YesYesHTTP, refreshing state
shareReplay({ bufferSize: 1, refCount: false })YesNoPersistent cache
publish() + connect()NoExplicitLive connection
AspectColdHot / Shared
ProducerPer subscriberOne
ValuesIndependentShared
Late subscriberGets allDepends on operator
UseOne consumerMultiple consumers

Key takeaways:

  • A cold Observable runs its producer for each subscriber; a hot one shares — the distinction determines whether work is duplicated
  • share() is the simplest multicast — the source runs when the first subscriber arrives and stops when the last leaves, with no replay
  • shareReplay({ bufferSize: 1, refCount: true }) is the HTTP pattern — the request is sent once, the result is cached, and late subscribers receive it
  • The refCount option determines whether the cache resets — true for refreshing data, false for a persistent cache
  • The duplicate request problem is the most common sharing bug — a component and its template subscribing to the same cold Observable send two requests
  • Store the shared Observable as a property, not a method — a method returns a new Observable on each call, and the sharing is lost
  • takeUntilDestroyed belongs in the subscriber’s pipeline, not in the shared source — placing it in the shared source completes the shared stream when one component is destroyed
  • publish and connect give explicit control — the source runs independently of the subscriber count, which is correct for a live connection
  • The lower-level operators are rarely needed — share and shareReplay cover most cases, and the explicit forms are for the cases where the connection must be decoupled
  • Sharing is opt-in because cold is the simpler default — the mental model of one producer per subscriber is easier, and sharing introduces the question of when the shared subscription starts and stops

Remember: Multicasting is how a single Observable serves many consumers without duplicating the work. share for events, shareReplay for cached values, publish and connect for explicit control. The refCount option is the decision between refreshing and caching, and the duplicate request problem is the most common symptom of a missing share. In Angular, the shared HTTP Observable and the shared view model are the patterns that appear in every application.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!