Rate-Limiting Requests with RxJS
Recently, I had an issue in a front-end application in which a user was allowed to click around a calendar and begin loading any day that was tapped. Each day triggered a semi-expensive API call, which could take a second or two to complete. The user was free to click around on more days and trigger more loads while data was fetched. Eventually, every request would be returned one-by-one. From the front-end’s perspective, all of this was groovy. The backend, on the other hand, started to get backed up, and requests would start failing if a user madly tapped around for long enough on enough calendar days.
We could’ve addressed this in many ways, but we settled on a quick rate-limiting solution on the frontend. This turned out to be a fairly simple refactor with RxJS:
- Capture an API request, rather than immediately subscribing to it.
- Queue that API request into a
BehaviorSubject
. - Subscribe to the
BehaviorSubject
of queued requests usingmergeAll
with a desired concurrency parameter.
Here’s a complete code sample (we’ll go into detail below).
If you run this, you should see two console logs every two seconds, similar to:
> 1:52:42 PM: 1
> 1:52:42 PM: 2
> 1:52:44 PM: 3
> 1:52:44 PM: 4
> 1:52:46 PM: 5
> 1:52:46 PM: 6
> 1:52:48 PM: 7
> 1:52:48 PM: 8
> 1:52:50 PM: 9
> 1:52:50 PM: 10
This example creates an Observable
of a sequence containing a single number, from one to ten, with each delayed by two seconds, the result of which is logged to the console with a timestamp. Each request$
is passed to the queue$
via next()
.
The rate-limiting magic comes from mergeAll
and its concurrency parameter (two, here). With this operator piped in, two requests will be subscribed to and fulfilled before the next two begin (error-handling logic left to the reader, as usual). I also piped in a filter
and map
, so I can initialize the subject without a value and subscribe to it immediately without processing any junk results.
That’s really all there is to it. If you’re working in an Angular environment, you can verify that this works easily enough using fakeAsync
and tick
from @angular/core/testing
, which I very much appreciate.