What You Will Learn

  1. The basics of HTTP caching
  2. Implementing caching with just two RxJS operators
  3. A step-by-step real-world caching example
  4. Key takeaways and best practices

HTTP caching is a mechanism where the browser stores local copies of web resources, so that subsequent requests for the same resource can be served faster without hitting the server again.

This post demonstrates how to set up caching using only publishReplay() and refCount() from RxJS — nothing more.

Searching for RxJS caching solutions online will typically surface two main recommendations: shareReplay() or a combination of publishReplay() with refCount(). Essentially, shareReplay() acts like publishReplay() but already includes refCount() behavior (see source A and source B). For this tutorial, we will work with the latter approach.

If you're curious about the finer distinctions between these operators, there is a detailed write-up worth reading.

Caching was originally designed to close the speed gap between the CPU and RAM. By holding data likely to be accessed again shortly, it avoids the latency of fetching from slower storage. The benefit becomes evident when you reload a webpage and it appears almost instantly — that is caching at work.

Why bother with caching? Cache memory sits inside the CPU, which makes it far quicker to access than RAM. Think of it like this: instead of driving to the hardware store for a hammer, you pick up the one in your toolbox at home. The shorter the distance, the faster the fetch.

In practice, there are several ways to implement caching. You might use NgRx to manage application state, the JavaScript localStorage API, or just a simple array to retain values. Some methods are straightforward, while others can introduce complexity, especially when you need to update or invalidate the cached data.

2. Caching with RxJS (publishReplay, refCount)

To respond to an HTTP call from the cache, you only need two RxJS operators: publishReplay() and refCount().

import { from } from 'rxjs'; 
import { map, publishReplay, refCount } from 'rxjs/operators';


// Create observable that holds two values
const observable$ = from(['first', 'last']).pipe(
  publishReplay(1), 
  refCount()
)

// First time subscribing, we get both values
observable$.subscribe(data => console.log(data));

// Second time subscribing, we get the latest value
observable$.subscribe(data => console.log(data));
observable$.subscribe(data => console.log(data));
observable$.subscribe(data => console.log(data));

// OUTPUT
// first
// last
// last
// last
// last

caching with a ReplaySubject

Notice that on the initial subscription, we receive two emissions: first and last. On any subsequent subscription, regardless of how often, we get only the last value. This behavior is the essence of caching.

This technique helps cut down on server traffic. How does it work? Rather than dispatching a new HTTP request every time, the request is sent only once. Each later subscription taps into the stored value rather than making an additional round trip to the server.

To get the most out of this caching pattern, a bit of extra control logic is needed, but the core concept here is the key to reducing redundant calls. In the next part, we’ll walk through a concrete example.

Before diving in, it’s worth asking: why combine publishReplay() with refCount()?

The publishReplay operator

By using publishReplay(), a cold observable is changed into a hot one through multicasting. Internally, it creates a new ReplaySubject() instance. The number you supply as an argument controls how many of the most recent values are replayed. For example, with an array like ['A', 'B', 'C'], calling publishReplay(1) replays just C — the latest item. If you use publishReplay(2), the last two values (B and C) are replayed.

The refCount operator

refCount() monitors the number of active subscribers. When all subscribers have unsubscribed, it automatically ceases the underlying source subscription, as explained in this note.

3. A practical example of how to cache

Picture this: you have a dashboard page that loads a JSON configuration file from the server. This configuration file drives which components appear on the page. Although the file itself updates infrequently (perhaps weekly), the data it returns is essentially stable.

That seems fine, but imagine thousands of users each visiting the page many times. Every visit would trigger a fresh request for the same configuration file, leading to an enormous number of redundant server calls.

This is neither cost-effective nor efficient from a performance standpoint. We need a mechanism to reuse the data instead of issuing repeated requests.

The snippet below shows how to build this cache using RxJS within a service. Keep in mind that while this example is tailored to Angular, the same pattern works with any observable library that provides these two operators.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map, publishReplay, refCount } from 'rxjs/operators';

export interface Config {
    componentType: string, 
    show: Boolean
}

@Injectable({
    providedIn: 'root'
})

export class ConfigService {

    configs: Observable<Config[]>;

    constructor(private httpClient: HttpClient) { }

    // Get configs from server | HTTP GET
    getConfigs(): Observable<Config[]> {

        // Cache it once if configs value is false
        if (!this.configs) {
            this.configs = this.httpClient.get(`${api_url}/configs`).pipe(
                map(data => data['configs']),
                publishReplay(1), // this tells Rx to cache the latest emitted
                refCount() // and this tells Rx to keep the Observable alive as long as there are any Subscribers
            );
        }

        return this.configs;
    }

    // Clear configs
    clearCache() {
        this.configs = null;
    }

}

As you can see, the caching logic lives in the service layer, which is the right place for it. Then, as usual, you can retrieve the data by subscribing manually, or let Angular's async pipe handle the subscription automatically.

3.1. Create a variable reference

This class-level variable acts as a shared reference across methods within the same service. It's also essential when clearing the cache.

... 

export class ConfigService {

    configs: Observable<Config[]>;

    ....

}

3.2. The cache logic

On the first call to getConfigs(), the result is stored for later use. A simple if-statement checks whether this.configs has been set. If it hasn’t, a GET request is dispatched to the server. If it has, the request is skipped in favor of the cached value.

// Get configs from server | HTTP GET
getConfigs(): Observable<Config[]> {

  // Cache it once if configs value is false
  if (!this.configs) {
    this.configs = Observable$.pipe(
      // ... operators
      publishReplay(1),
      refCount()
     );
  }
  return this.configs;
}

3.3. Clearing the cache

To refresh the cached data, assign this.configs to null. The next time getConfigs() runs, it will detect the empty reference, send a new request, and store the latest result in the cache.

// Clear configs
clearCache() {
  this.configs = null;
}

4. Summary

When data changes infrequently — daily, weekly, or less often — caching is a solid strategy for avoiding unnecessary server load. However, misuse can lead to stale data or performance issues, so caution is advised. Cache storage is limited (kilobytes to megabytes), and its purpose is to hold data temporarily.

If you'd like to strengthen your grasp of Subjects and Observables, these articles on reactive programming are a great starting point.

[

An introduction to observables in Reactive Programming

Grasping the observer pattern is often a hurdle for developers new to the field. Using RxJS to manage asynchronous data — be it user interactions, HTTP responses, or time-based events — requires a solid understanding of subscribing and waiting for results. For most, this is where the difficulty lies ...

Fastest way to cache for lazy developers — Angular with RxJS — figure 1freeCodeCamp.orgDler Ari

Fastest way to cache for lazy developers — Angular with RxJS — figure 2

](https://medium.freecodecamp.org/an-introduction-to-observables-in-reactive-programming-1cfd3e23bb94)

[

An introduction to Subjects in Reactive Programming

A Subject is a unique kind of observable designed for broadcasting values to a group of subscribers. Its real-time nature is a standout feature. For instance, if a subject has ten subscribers and we push a value to it, that value is instantly visible to all of them ...

Fastest way to cache for lazy developers — Angular with RxJS — figure 3freeCodeCamp.orgDler Ari

Fastest way to cache for lazy developers — Angular with RxJS — figure 4

](https://medium.freecodecamp.org/an-introduction-to-subjects-in-reactive-programming-bbdc8fed7b6)