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:

  1. Capture an API request, rather than immediately subscribing to it.
  2. Queue that API request into a BehaviorSubject.
  3. Subscribe to the BehaviorSubject of queued requests using mergeAll 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.


Related posts