The Best New Way To Cache API Responses with Angular and RxJs
Let's learn the best new way to implement time based caching for the API responses (or any other) RxJs streams in our Angular applications!

The time has come, clean RxJs time-based caching is now possible! đž by Elena Koycheva & đš by Tomas Trajan
Hey folks! Welcome đ
This article is a rather special one!
Over the years, Iâve repeatedly got caught up in trying to implement this use case in a clean way. Often involving many colleagues, spending a bit too much time, but always without proper success.
Even though we could always come up with a working solution , the solution we got felt down right dirtyâŠ
Until nowâŠ
Today weâre going change that and learn about the best new way to implement time based caching for the API responses (or any other) RxJs streams in our Angular applications!
â This article is pretty focused on a single topic, so you should be able to get through it in one go, still TLDR; canât hurt nobody đ
TLDR
- Original example use case of retrieving and caching the
apiKey
- Previous approaches how to solve it (before RxJs 7.1) and their flaws
- New better solution with the help from improved
share
operator made available in RxJs 7.1+ - Refactoring our original implementation
- Caveats, gotchas and comparing possible solutions and their trade-offs
- Working solution (StackBlitz) & Cheat Sheet
The Original Use Case
Letâs imagine an Angular application where in order to retrieve some data from a API endpoint (aka backend) we have to provide two HTTP headers:
- Standard
access-token
, eg JWT token which we retrieve when we sign in - Custom
api-key
which we have to retrieve from its dedicated endpoint (and we can because we already have theaccess-token
which is sufficientâŠ)
Besides that, the
api-key
has time restricted validity which is not really predictable, please donât ask me why⊠Letâs just say it will always be valid for at least 10 seconds after it was retrieved, potentially all the way up to one month đ đ
Letâs explore what are our options to solve this use caseâŠ
đRetrieve key for every request we make
Our Angular interceptor which sets HTTP headers could retrieve fresh API key for every request we make
- â We can be sure that the none of our requests will fail because weâre using outdated API keyâŠ
- â Getting a fresh API key for every request will delay that request significantly because it we will effectively have to always make two requests instead of just one
// api key service
@Injectable({ providedIn: 'root' })
export class ApiKeyService {
// cold stream definition
apiKey$ = this.httpClient.get<string>(API_KEY_ENDPOINT);
constructor(private httpClient: HttpCLient) {}
}
// auth interceptor
export class AuthInterceptor implements HttpInterceptor {
constructor(
private accessTokenService: AccessTokenService,
private apiKeyService: ApiKeyService,
) {}
intercept(
request: HttpRequest<any>,
next: HttpHandLer,
): Observable<HttpEvent> {
const accessToken = this.accessTokenService.getAccessToken();
return apiKeyService.apiKey$.pipe(
// will trigger extra backend request
concatMap((apiKey) => {
request = request.clone({
setHeaders: {
Authorization: `Bearer ${accessToken}`,
'x-api-key': apiKey,
},
});
return next.handle(request);
}),
);
}
}
Example of a solution which will trigger additional request to get fresh API key for every real request
In the example above, the ApiKeyService
exposes the apiKey$
stream definition (cold stream) which will be then executed for every real request in the intercept()
method of our AuthInterceptor
.
đRetrieve and cache API key forever. After that just close your eyes and hope for the best đ€and retry requests that failed because the API key was invalid đ
- â Good for performance, the API key is retrieved initially and then cached, every subsequent real request will reuse the cached API key, butâŠ
- â The request might fail because the API key became invalid, in that case we have to implement extra logic which will re-fetch API key and re-try the failed request which is a lot of additional code to implement and maintainâŠ
đ Refresh key periodically
- â We can be sure that the none of our requests will fail because weâre using outdated keyâŠ
- â Getting API key periodically can be bad for backend performance and even costly in case of a cloud based solution. Imagine we have thousands of concurrent users who leave the tab with application open even though they are not really interacting with the application. They donât do any real request but we will still re-fetch new API key every minute or soâŠ
@Injectable({ providedln: 'root' })
export class ApiKeyService {
apiKey: string; // will be accessed sync with apiKeyService.apiKey
constructor(private httpCtient: HttpCtient) {
this.timer(0, 60_000)
.pipe(switchMap(() => this.httpClient.get<string>(API_KEY_ENDPOINT)))
.subscribe((apiKey) => {
this.apiKey = apiKey;
}); // no need to unsubscribe, global singleton
// should run for the whole app life time...
}
}
Example of solution where we refresh API key on interval (eg one minute)
Now is the time to explore the first real solution for our API key caching use case!
đRetrieve key in a lazy way (only when there are requests) and cache it only for a short period of time
- â No unnecessary requests when user doesn't use the application (eg leaves the tab open)
- â No performance penalty, requests which happen in quick succession will reuse the same API key (we don't retrieve fresh API key for every request)
- â No need to implement special retry logic because the key is guaranteed to be fresh enough to rule out that scenario
- â Problematic implementation, manually managed local state, not a self contained RxJs stream based solution, the stream has to be accessed with a factory instead of just public property accessâŠ
@Injectable({ providedln: 'root' })
export class ApiKeyService {
apiKey$: Observable<string>;
constructor(private httpCtient: HttpCtient) {}
getApiKey() {
if (this.apiKey$) {
// if key exists
return this.apiKey$; // return cachec API key
} else {
this.apiKey$ = this.httpClient
.get<string>(API_KEY_ENDPOINT)
.pipe(shareReplay(1)); // retrieve new API key
setTimeout(() => {
// setup cache invalidation
this.apiKey$ = undefined; // unset stream
}, CACHE_TIMEOUT); // cache invalidation timeout
}
}
}
Example of a solution which implements time-based stream caching. The solution works but it requires manual local state management in contrast with a desired self contained RxJs streamâŠ
Problems with our custom RxJs caching solution
The solution above works just fine but itâs not really nice or _clean_âŠ
Weâre manually managing local state with imperative logic around our RxJs stream which just doesnât feel rightâŠ
It is always* possible to define RxJs stream with all the sources of change from start without the need to re-create streams during runtime
*the statement above holds true, in last 6 years, this was the only use case I was not able to solve without re-creating stream during runtime đ
But donât worry, everything changes with the advent of RxJs 7.1 and its new improved share
operator which will allow us to do it right! đȘ
Follow me on Twitter because you will get notified about new Angular blog posts and cool frontend stuff!đ
The new improved share operator
RxJs 7.1 brought us new improved share operator and especially more powerful way to configure it!
Letâs refactor our last local state based caching solutionâŠ
const CACHE_TIMEOUT = 10 * 1000; // 10 seconds
@Injectable({ providedln: 'root' })
export class ApiKeyService {
apiKey$ = this.httpClient.get<string>(API_KEY_ENDPOINT).pipe(
tap(() => console.log('[DEBUG] request happened')),
share({
// HttpClient.get is a completing stream
// eg '---a|' (marble diagram)
resetOnComplete: () => timer(CACHE_TIMEOUT),
// as it completes, we start a timer which will reset the stream
// when finished, this means that the last API key value will be
// shared with all subscribers until the timer is triggered which
// is the desired time-based caching behavior
}),
);
constructor(private httpCtient: HttpCtient) {}
}
Example of a new improved pure RxJs based solution to time-based caching for our API key use case
- â In this solution we do NOT need any kind of local state or a stream factory method, everything is nicely self-contained within the RxJs stream
- â It is enough to just store a stream definition in a public property of the service which then can be accessed with simple apiKeyService.apiKey$ property access
- â The stream definition is cold (lazy) so it wont trigger any request until the first consumer subscribes to it! In our case it will wait until the application makes a first ârealâ request to some other endpoint
Letâs see how our solution behaves during the runtime!
@Injectable()
class AuthInterceptor {
constructor(private apiKeyService: ApiKeyService) {}
intercept() {
// unrealistic, for demonstration purposes only
this.apiKeyService.apiKey$.subscribe(console.log); // logs: [DEBUG] request
// logs: apiKey1
this.apiKeyService.apiKey$.subscribe(console.log); // logs: apiKey1
this.apiKeyService.apiKey$.subscribe(console.log); // logs: apiKey1
this.apiKeyService.apiKey$.subscribe(console.log); // logs: apiKey1
setTimeout(() => {
this.apiKeyService.apiKey$.subscribe(console.log); // â ïž doesn't log anything !?
this.apiKeyService.apiKey$.subscribe(console.log);
}, 1000); // less
setTimeout(() => {
this.apiKeyService.apiKey$.subscribe(console.log); // logs: [DEBUG] request
// logs: apiKey2
this.apiKeyService.apiKey$.subscribe(console.log); // logs: apiKey2
}, 11_000); // more than caching timeout
}
}
Example of a timeline of what could happen with our new pure RxJs time-base stream caching solution
As we can see, the first subscription triggers the request to retrieve the API key and the subsequent subscriptions receive the cached API key without triggering of another request, great!
Once enough time has passed (more than the cache timeout) next subscription will trigger another request, the new value will be cached againâŠ
The whole process will keep repeating itself as long there are more subscriptions (in our case requests handled by the interceptor) so only as long as user interacts with our app which is as lazy as it gets đ
â ïž Still, the solution doesnât work perfectly yetâŠ
The subscriptions which happened with delay which was still within the caching window but after the initial subscription didnât trigger new request â but they also didnât receive any API key â ⊠(look for the â ïž icon in the code example above)
Letâs fix this behavior with our last change for today
The share
operator uses a RxJs subject behind the scenes as the Subject
is the RxJs way to implement multicasting of an Observable.
This behavior of the share
operator can be adjusted by overriding its connector
option.
By default, the connector
will use basic RxJs Subject
which emits events in mode which is best described as âfire and forgetâ. This is the reason why subscribers who subscribed to our stream of API keys after some delay did not receive any API key, the event happened in the past and the basic Subject
does not have any mechanism to remember the last emitted value!
Luckily, this can be quickly fixed by overriding connector
and using another RxJs subject called ReplaySubject
.
The desired behavior then can be achieved with connector: () => new ReplaySubject(1)
as weâre only interested in the latest API key!
Letâs see it in action đ
const CACHE_TIMEOUT = 10 * 1000; // 10 seconds
@Injectable({ providedln: 'root' })
export class ApiKeyService {
apiKey$ = this.httpClient.get<string>(API_KEY_ENDPOINT).pipe(
tap(() => console.log('[DEBUG] request happened')),
share({
// fix the problem where later subscribers
// did not receive cached API key
connector: () => new ReplaySubject(1), // override default "new Subject()"
resetOnComplete: () => timer(CACHE_TIMEOUT),
}),
);
constructor(private httpCtient: HttpCtient) {}
}
Finished example with properly working solution which behaves exactly as we want!
Letâs see how our fixed solution behaves during the runtimeâŠ
@Injectable()
class AuthInterceptor {
constructor(private apiKeyService: ApiKeyService) {}
intercept() {
// unrealistic, for demonstration purposes only
this.apiKeyService.apiKey$.subscribe(console.log); // logs: [DEBUG] request
// logs: apiKey1
this.apiKeyService.apiKey$.subscribe(console.log); // logs: apiKey1
this.apiKeyService.apiKey$.subscribe(console.log); // logs: apiKey1
this.apiKeyService.apiKey$.subscribe(console.log); // logs: apiKey1
setTimeout(() => {
this.apiKeyService.apiKey$.subscribe(console.log); // â
logs: apiKey1
this.apiKeyService.apiKey$.subscribe(console.log); // â
logs: apiKey1
}, 1000); // less
setTimeout(() => {
this.apiKeyService.apiKey$.subscribe(console.log); // logs: [DEBUG] request
// logs: apiKey2
this.apiKeyService.apiKey$.subscribe(console.log); // logs: apiKey2
}, 11_000); // more than caching timeout
}
}
The subscriptions which happen with delay which is still within the caching window but after the initial subscription didnât trigger new request â and correctly receives the latest API key â hooray! đ
Check out the working solution in StackBlitz!
Quick cheat sheet to share with your colleaguesđ

BONUS: Why did we use ReplaySubject instead of the more common BehaviorSubject?
The last step of our implementation was to override the connector
option which is using new Subject()
by default with the connector: () => new ReplaySubject(1)
.
That approach fixed our issue where later subscribers did not receive last stored API key because default Subject
handles events in a way which is best described with âfire and forgetâ.
Some of you might be wondering why did we choose new ReplaySubject(1)
instead of new BehaviorSubject('')
which is a very interesting question which will bring us further insights!
- â
Both subjects store last stream value and make it available for the subscribers who subscribe AFTER the value was originally emitted, that way we do not lose the value as was the case with plain
Subject
- â
The
ReplaySubject(1)
will store last emitted value and make it available for the future subscribes, but it does **NOT **need an initial value - â ïž The
BehaviorSubject('some initial value')
mandates an initial value which it will provide for the new subscriber immediately. In our case it would either lead to and error as the'some initial value'
is not a valid API key, or we would need to provide additionalfilter(apiKey => apiKey !== 'some initial value')
which would prevent itâŠ
We made it! đȘ
I hope You enjoyed learning about the best way to cache backend API responses in your Angular applications with the help of new better share operator available from RxJs 7.1.
Now go and start caching your streams where it makes sense to deliver faster apps and safe network bandwidth for your users!
Also, donât hesitate to ping me if you have any questions using the article responses or Twitter DMs @tomastrajan
And never forget, future is bright

Do you enjoy the theme of the code preview? Explore our brand new theme plugin
Skol - the ultimate IDE theme
Northern lights feeling straight to your IDE. A simple but powerful dark theme that looks great and relaxes your eyes.
Do you enjoy the content and think that your teammates or organization could benefit from more direct support?

Angular Enterprise Architecture Ebook
Learn how to architect and scaffold a new enterprise grade Angular application with clean, maintainable and extendable architecture in almost no time!
Lots of actionable tips and pros & cons of specific decisions based on the extensive experience!
Get notified
about new blog posts
Sign up for Angular Experts Content Updates & News and you'll get notified whenever I release a new article about Angular, Ngrx, RxJs or other interesting Frontend topics!
We will never share your email with anyone else and you can unsubscribe at any time!

Tomas Trajan
GDE for Angular & Web Technologies
I help developer teams deliver successful Angular applications through training and consulting with focus on NgRx, RxJs and Nx!
Ein Google-Developer expert fĂŒr Angular und Webtechnologien, der als Berater und Angular-Trainer arbeitet. Derzeit unterstĂŒtzt er weltweit Teams in Unternehmen bei der Implementierung von KernfunktionalitĂ€ten, Architekturen, der EinfĂŒhrung von Best Practices, und der Optimierung von ArbeitsablĂ€ufen.
Tomas ist stÀndig bestrebt, seinen Kunden und der breiteren Entwicklergemeinschaft einen maximalen Nutzen zu bieten. Seine Arbeit wird durch eine umfangreiche Erfolgsbilanz unterstrichen, in der er populÀre Fachartikel veröffentlicht, VortrÀge auf internationalen Konferenzen und Meetups hÀlt und zu Open-Source-Projekten beitrÀgt.
45
Blog posts
4M
Blog views
3.5K
Github stars
500
Trained developers
30
Given talks
8
Capacity to eat another cake
Responses
Do not hesitate to ask questions and share your own experience and perspective with the topic
You might also like
Check out following blog posts from Angular Experts to learn even more about related topics like RxJs or Angular !

Angular Material components testing
How and why to use Angular Materials component harness to write reliable, stable and readable component tests

Angular & tRPC
Maximum type safety across the entire stack. How to setup a fullstack app with Angular and tRPC.

Jest ESM - Total Guide To More Than 100% Faster Testing For Angular âĄ
Let's learn how to speed up our Angular Jest tests by more than 100% by switching to Jest ESM (a notoriously problematic migration) and how to solve all the troublesome issues that tend to pop up along the way!
StÀrken Sie Ihr Team mit unserer umfassenden Erfahrung
Unsere Angular Experten haben viele Jahre damit verbracht, Unternehmen und Startups zu beraten, Workshops und Tutorials zu leiten und umfangreiche Open-Source-Ressourcen zu pflegen. Wir sind sehr stolz auf unsere Erfahrung im Bereich des modernen Frontends und wĂŒrden uns freuen auch Ihrem Unternehmen zum Aufschwung zu verhelfen.