Angular: HTTP Caching with RxJS shareReplay

Published: (February 16, 2026 at 08:08 AM EST)
7 min read
Source: Dev.to

Source: Dev.to

Caching HTTP Calls in Angular

Repeated HTTP calls in an Angular application quickly become a performance problem: slower page loads, unnecessary backend load, and wasted network traffic.
The obvious solution is caching, but in Angular caching is not just about storing data – it’s about managing observables correctly.

If done wrong, you’ll get duplicate requests, stale data, or permanently cached errors.
If done right, you eliminate redundant calls entirely.


1. Naïve Cache Using a Service Field

@Injectable({
  providedIn: 'root',
})
export class AppService {
  private vehiclesDataCache: VehiclesApiResponse | null = null;

  constructor(private http: HttpClient) {}

  public getVehiclesData(): Observable {
    if (this.vehiclesDataCache) {
      return of(this.vehiclesDataCache);
    }

    const observable = this.http.get(
      'https://starwars-databank-server.vercel.app/api/v1/vehicles',
    );

    // Store the response for later calls
    observable.subscribe((response) => {
      this.vehiclesDataCache = response;
    });

    return observable;
  }
}

Problems with this approach

  • If getVehiclesData() is called multiple times before the first request finishes, each call creates a new HTTP request.
  • The manual subscription is never unsubscribed → potential memory leaks.
  • The code mixes data‑fetching logic with caching logic, making it harder to maintain.

2. Using shareReplay – The RxJS Way

shareReplay caches the latest emitted value from an observable so that multiple subscribers share the same source without triggering additional backend calls.

@Injectable({
  providedIn: 'root',
})
export class AppService {
  private vehiclesDataShareReplay$: Observable | null = null;

  constructor(private http: HttpClient) {}

  public getVehicleShareReplay(): Observable {
    if (!this.vehiclesDataShareReplay$) {
      this.vehiclesDataShareReplay$ = this.http
        .get('https://starwars-databank-server.vercel.app/api/v1/vehicles')
        .pipe(shareReplay(1));   // cache the last emission
    }
    return this.vehiclesDataShareReplay$;
  }
}

The subscription to the HttpClient remains active only until the request completes.
This pattern is the most common way to cache HTTP requests in Angular services.

2.1. Pitfall – Errors Are Cached Too

shareReplay also replays errors. If the request fails, every future subscriber receives the same error and no new request is ever made.


3. Handling Errors – retry + catchError

A typical solution is to retry the request and reset the cached observable when an error occurs:

@Injectable({
  providedIn: 'root',
})
export class AppService {
  private vehiclesDataShareReplay$: Observable | null = null;

  constructor(private http: HttpClient) {}

  public getVehicleShareReplay(): Observable {
    if (!this.vehiclesDataShareReplay$) {
      this.vehiclesDataShareReplay$ = this.http
        .get('https://starwars-databank-server.vercel.app/api/v1/vehicles')
        .pipe(
          retry(2), // try up to 3 times total
          catchError((error) => {
            // Reset cache so the next subscriber can retry
            this.vehiclesDataShareReplay$ = null;
            return throwError(() => error);
          }),
          shareReplay(1),
        );
    }
    return this.vehiclesDataShareReplay$;
  }
}
  • retry(2) → three attempts in total.
  • catchError clears the cached observable before re‑throwing the error, allowing a fresh request on the next subscription.

4. Cache‑With‑Refresh Pattern

shareReplay(1) works great for static data, but the cached value can become stale.
A common pattern is to combine a BehaviorSubject (as a refresh trigger) with switchMap and shareReplay.

@Injectable({
  providedIn: 'root',
})
export class AppService {
  private vehiclesDataBehavior$: Observable | null = null;
  private refreshTrigger$ = new BehaviorSubject(undefined); // initial trigger

  constructor(private http: HttpClient) {}

  /** Observable that always emits the latest successful response */
  public getVehicleBehavior(): Observable {
    // Create the observable only once
    if (!this.vehiclesDataBehavior$) {
      this.vehiclesDataBehavior$ = this.refreshTrigger$.pipe(
        // Every time the trigger emits, perform a new HTTP request
        switchMap(() =>
          this.http
            .get('https://starwars-databank-server.vercel.app/api/v1/vehicles')
            .pipe(
              retry(2),
              catchError((error) => {
                console.error('Error fetching vehicles:', error);
                // Swallow the error for this emission; keep the previous cached value
                return EMPTY;
              }),
            ),
        ),
        // Cache the most recent successful response and replay it to all subscribers
        shareReplay(1),
      );
    }
    return this.vehiclesDataBehavior$;
  }

  /** Call this method to force a refresh */
  public refreshVehiclesBehavior(): void {
    this.refreshTrigger$.next(undefined);
  }
}

How it works

  1. Initial creation – The observable is built once and stored in vehiclesDataBehavior$.
  2. Refresh triggerrefreshTrigger$ emits whenever refreshVehiclesBehavior() is called.
  3. switchMap – Cancels any in‑flight request and starts a new one on each trigger.
  4. Error handlingretry(2) attempts the request up to three times; on final failure we log the error and return EMPTY so the previous cached value stays alive.
  5. shareReplay(1) – Keeps the latest successful response and replays it to any current or future subscriber.

5. Summary

TechniqueWhen to useProsCons
Simple field cacheVery small, one‑time dataEasy to understandNo sharing of in‑flight requests, manual subscription → leaks
shareReplay(1)Static reference data (enums, config)Minimal code, automatic sharingCaches errors; data can become stale
shareReplay + retry/catchErrorSame as above, but you need resilienceErrors don’t permanently block future callsSlightly more boilerplate
BehaviorSubject + switchMap + shareReplayData that may need periodic refreshAllows explicit refresh while still cachingMore moving parts; requires a trigger

By leveraging RxJS operators correctly, you can keep your Angular app fast, reduce backend load, and avoid the hidden pitfalls of naïve caching.

Error Handling in the Cached Observable Pattern

When an error occurs we cannot simply reset the cached observable and re‑throw the error like this:

catchError((error) => {
  this.vehiclesDataShareReplay$ = null;
  return throwError(() => error);
})

Why throwError Is Problematic

  • throwError() inside a switchMap completes the entire stream.
  • Once completed, the stream is dead forever – any future next() calls on the BehaviorSubject have no effect because there is no active subscription.
  • Setting vehiclesDataBehavior$ = null does not revive the stream; the pipeline has already terminated.

Using EMPTY Instead

When an error occurs, EMPTY completes the inner observable without emitting a value. Because the outer stream stays alive and continues listening to the BehaviorSubject, we get:

  • Subscribers are NOT notified of the error – they retain the last successful cached value.
  • The refresh mechanism stays active – users can trigger another fetch with refreshVehiclesBehavior().

Trade‑off

  • Users see stale data instead of an error message.
  • If you need to surface errors, use a different approach (e.g., Subject to model both success and failure states).

shareReplay Configuration

Understanding how different shareReplay configurations affect subscriptions and memory usage is essential to avoid leaks.

ConfigurationBufferSubscription BehaviourTypical Use‑Case
shareReplay() (no args)Caches all emitted values (infinite buffer)Stays active until the source completes, even with zero subscribersFinite streams where you need the full emission history
shareReplay(1)Caches the last emitted valueRemains active until the source completesGlobal singleton data fetched once and reused across the app (most common for HTTP requests)
shareReplay({ bufferSize: 1, refCount: true })Caches the last emitted valuerefCount: automatically unsubscribes from the source when subscriber count drops to 0Long‑lived or infinite streams (WebSockets, timers) or when you want cached data to reset when users leave a feature

When refCount Matters

  • Cleanup: Useful for streams that should stop when nobody is listening (e.g., WebSocket connections).
  • Component‑level data: Not always needed; a simple shareReplay(1) often suffices.

When not to Use shareReplay

  • User‑specific data – risk of leaking one user’s cached data to another.
  • Real‑time data – stock prices, live scores, etc., need fresh data on every request.
  • Large payloads – caching big responses inflates memory usage.
  • Security‑sensitive data – ensure proper cache invalidation on logout.

Key Take‑aways for Caching in Angular

  1. Purpose – Caching isn’t just about performance; it also controls side effects, avoids duplicate work, and makes state management predictable.
  2. shareReplay(1) – Ideal for global, static data that rarely changes.
  3. Error handling – Must be implemented; otherwise failures become permanently cached.
  4. Stale data – Requires an explicit refresh strategy (e.g., a BehaviorSubject combined with switchMap).
  5. refCount – Important for long‑lived or infinite streams and for aligning cache lifetime with component lifecycles.
  6. No one‑size‑fits‑all – Choose the configuration based on data longevity, consumer scope, and refresh requirements.

When used deliberately, shareReplay can eliminate redundant HTTP calls, simplify state management, and significantly improve application performance—without introducing hidden bugs or memory leaks.


Example Project

A working example is available in my 📁 GitHub repository. Feel free to explore the code for a concrete implementation of the patterns discussed above.

0 views
Back to Blog

Related posts

Read more »

Redesigned my space on the internet.

Wanted it to be fast, accessible, and easy to navigate. Tech Stack - Next.js 14 App Router - Zero layout shifts - Server Actions for emails Live here: Tags: Rea...