Rate limiting caps how many operations may start in a period of time, for example at most 2 requests per second. Many APIs enforce a limit like this and refuse requests over it, often with the HTTP status 429 Too Many Requests. A rate limiter in your own code keeps you under the limit. Calls over it usually aren't dropped: they wait in a queue, in the order they were made, until there's room.
"Per second" can be counted in two ways. With fixed windows, time is cut into blocks, 0 to 999 ms, 1000 to 1999 ms, and so on, and the count starts again at the beginning of each block. With a sliding window, a call may start only if fewer than the limit started in the last 1000 ms, counting back from now. Here are four calls under a limit of 2 per 1000 ms:
| Call made at | Fixed windows | Sliding window |
|---|---|---|
| 900 ms | Starts at once. | Starts at once. |
| 950 ms | Starts at once. | Starts at once. |
| 1000 ms | Starts at once: a new block has begun, so the count is back to 0. | Waits until 1900 ms, when the call from 900 ms is 1000 ms old. |
| 1050 ms | Starts at once. | Waits until 1950 ms, when the call from 950 ms is 1000 ms old. |
With fixed windows, four calls started within 150 ms, twice the limit, because two fell at the end of one block and two at the start of the next. A sliding window never lets more than 2 start in any 1000 ms, wherever those 1000 ms begin.
Rate limiting is easy to mix up with two other ways of holding calls back:
| Technique | What it counts | What happens to extra calls |
|---|---|---|
| Rate limit | Calls started in the last period of time, finished or not | They wait in a queue until there's room. |
| Throttle | Runs of one function, at most one per period | They're dropped, though some throttles run the latest one at the end of the period. |
| Concurrency limit | Calls still running right now | They wait until a running call finishes. |
So a request that takes 5 ms frees its place under a concurrency limit straight away, but under a sliding-window rate limit of 2 per 1000 ms it counts for the full 1000 ms after it started.
Write rateLimit(fn, limit, interval). It returns a function that calls fn, but never starts more than limit calls in any interval ms: a call may start only if fewer than limit calls started in the last interval ms. A call that started exactly interval ms ago no longer counts.
When there's room, fn runs right away, during the call. Otherwise the call waits in a queue and runs as soon as there's room, in the order the calls were made, with its own this and arguments. A call counts from the moment it starts, whether fn succeeds, fails, or is still running.
Every call returns a promise of what fn returns. If fn throws, that promise rejects with the error, and the queue carries on.
const send = rateLimit((message) => api.post(message), 2, 1000);
send("a"); // starts now
send("b"); // starts now
send("c"); // starts at 1000 ms, when "a" is 1000 ms old
send("d"); // starts at 1000 ms, when "b" is 1000 ms old
send("e"); // starts at 2000 ms
useFakeTimers();
const fn = spy();
const limited = rateLimit(fn, 2, 1000);
limited(1);
limited(2);
limited(3);
expect(fn.calls.map((call) => call.args[0])).toEqual([1, 2]);
const clock = useFakeTimers();
const fn = spy();
const limited = rateLimit(fn, 2, 1000);
limited(1);
limited(2);
limited(3);
await clock.tickAsync(999);
expect(fn).toHaveBeenCalledTimes(2);
await clock.tickAsync(1);
expect(fn).toHaveBeenCalledTimes(3);
expect(fn).toHaveBeenLastCalledWith(3);
const clock = useFakeTimers();
const fn = spy();
const limited = rateLimit(fn, 2, 1000);
const started = () => fn.calls.map((call) => call.args[0]);
for (const value of [1, 2, 3, 4, 5]) limited(value);
expect(started()).toEqual([1, 2]);
await clock.tickAsync(1000);
expect(started()).toEqual([1, 2, 3, 4]);
await clock.tickAsync(1000);
expect(started()).toEqual([1, 2, 3, 4, 5]);
const clock = useFakeTimers();
const fn = spy();
const limited = rateLimit(fn, 2, 1000);
const started = () => fn.calls.map((call) => call.args[0]);
limited(1);
await clock.tickAsync(500);
limited(2);
limited(3);
limited(4);
expect(started()).toEqual([1, 2]);
await clock.tickAsync(500); // 1 is now 1000 ms old
expect(started()).toEqual([1, 2, 3]);
await clock.tickAsync(499);
expect(started()).toEqual([1, 2, 3]);
await clock.tickAsync(1); // 2 is now 1000 ms old
expect(started()).toEqual([1, 2, 3, 4]);
const clock = useFakeTimers();
const limited = rateLimit((value) => value * 10, 1, 1000);
let first;
let second;
limited(1).then((value) => {
first = value;
});
limited(2).then((value) => {
second = value;
});
await clock.tickAsync(0);
expect(first).toBe(10);
expect(second).toBeUndefined();
await clock.tickAsync(1000);
expect(second).toBe(20);
const clock = useFakeTimers();
const limited = rateLimit((value) => {
if (value === "bad") throw new TypeError("bad input");
return value;
}, 1, 1000);
let error;
let result;
limited("first");
limited("bad").catch((reason) => {
error = reason;
});
limited("ok").then((value) => {
result = value;
}, () => {});
await clock.tickAsync(1000);
expect(error).toBeInstanceOf(TypeError);
await clock.tickAsync(1000);
expect(result).toBe("ok");
const clock = useFakeTimers();
const fn = spy();
const limited = rateLimit(fn, 1, 1000);
const a = { limited };
const b = { limited };
a.limited();
b.limited();
await clock.tickAsync(1000);
expect(fn.calls[0].this).toBe(a);
expect(fn.calls[1].this).toBe(b);
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Rate limiter. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.