publishReplay – A Closer Look

The publishReplay operator builds on the publish operator by swapping out the standard Subject for a ReplaySubject. This allows a single subscription to the source stream to be distributed across multiple subscribers, while also replaying a buffer of prior values — even after the source has emitted its complete notification.

publishReplay is essentially a convenience wrapper around the multicast operator configured with a ReplaySubject. To fully grasp how it works, it's helpful to first understand the mechanics of multicast.

Consider this demonstration where publishReplay shares an underlying obs stream among three subscriptions. The first two subscribers connect before the stream finishes and thus see all three emissions. The third subscriber is set up with a timeout and joins after completion — despite the stream being closed, the new subscriber still receives the last two values along with the COMPLETE notification:

const log = (index) => (v) => console.log(`subscription ${index}\t: ` + v);

const obs = interval(200).pipe(
   take(3),
   tap({ complete() { console.log('underlying stream completed') }})
);

const shared = obs.pipe(
   publishReplay(2)
);

shared.subscribe(log(1));
shared.subscribe(log(2));

shared.connect();

setTimeout(() => shared.subscribe(log(3)), 1000);

The resulting output looks like this:

subscription 1	: 0
subscription 2	: 0
subscription 1	: 1
subscription 2	: 1
subscription 1	: 2
subscription 2	: 2
underlying stream completed
subscription 3	: 1
subscription 3	: 2

Compared to running the same example with plain publish, the subscription 3 block now contains two additional entries. Even though this late subscriber does not reactivate the source stream (which remains completed), the two final values are still delivered. This happens because they are stored in the ReplaySubject and replayed to any new observer upon subscription.

Since publishReplay is just a shortcut for multicast with a ReplaySubject, you can swap it out directly and see identical results:

const shared = obs.pipe(
   multicast(new ReplaySubject(2))
);

Like multicast, publishReplay simply inserts a ReplaySubject into the observable chain. It's the subject itself that performs the sharing — neither publishReplay nor multicast does that work directly; they just streamline the setup.

That said, there's a key difference. With multicast, you can supply your own Subject instance. With publishReplay, the ReplaySubject is instantiated behind the scenes and never exposed.

The multicast operator is also the more flexible option: it accepts a factory function, allowing a fresh ReplaySubject to be created on demand each time the source is (re)subscribed. The publishReplay operator, by contrast, relies on an internal instance that is created only once. More details on this distinction are available here.

To illustrate the difference, this next example uses multicast with a factory. The outcome changes because the underlying subscription gets re-armed:

const log = (index) => (v) => console.log(`subscription ${index}\t: ` + v);

const obs = interval(200).pipe(
   take(3),
   tap({ complete() { console.log('underlying stream completed') }})
);

const shared = obs.pipe(
   multicast(() => new ReplaySubject(2))
);

shared.subscribe(log(1));
shared.subscribe(log(2));

shared.connect();

setTimeout(() => { shared.subscribe(log(3)); shared.connect() }, 1000);

Running this snippet produces this output:

subscription 1	: 0
subscription 2	: 0
subscription 1	: 1
subscription 2	: 1
subscription 1	: 2
subscription 2	: 2
underlying stream completed
subscription 3	: 0
subscription 3	: 1
subscription 3	: 2
underlying stream completed

Notice how the third subscription now receives all three values, and the underlying stream emits its complete notification twice:

subscription 3	: 0
subscription 3	: 1
subscription 3	: 2
underlying stream completed

This behavior is a direct consequence of combining multicast with a factory function.