rate-limiter
A rate limiter that helps you limit your client from making excessive API requests.
Written in TypeScript and compatible with both Node.js and browsers.
Installation
With npm do:
$ npm install @mangosteen/rate-limiter
-
limiter does not offer any queue to order the requests. This means an unlucky task could be waiting forever, because other active and lucky tasks just keep consuming all the tokens. Our limiter comes bundled with a prioritized FIFO queue, so you can customize the priority of each task, and tasks with the same priority are completed in FIFO order.
-
bottleneck reservoirs do not support a rolling window. If you add 10 jobs just 1ms before the reservoir refresh is triggered, the first 5 jobs will run immediately, then 1ms later it will refresh the reservoir value and that will make the last 5 jobs also run right away, so you end up with 10 jobs running in parallel. Our limiter comes bundled with RollingWindowTokenBucket
that keeps a history of past jobs to ensure you only launch desired number of jobs in the last X
seconds no matter what.
Quick Start
Step 1 - Token Bucket
Create a token bucket. It holds tokens that represent a numeric cost of launching jobs. For example, you can create a bucket with 10 tokens. You can consume 1 token to launch a job with a cost of 1. Other jobs might consume more/less tokens, whatever makes more sense for your application.
import {
RollingWindowTokenBucket,
TokenBucket
} from '@mangosteen/rate-limiter';
const basicTokenBucket = new TokenBucket({
initialTokens: 10,
maxTokens: 20,
});
const windowTokenBucket = new RollingWindowTokenBucket({
initialTokens: 10,
maxTokens: 20,
historyIntervalMs: 5000,
maxHistoryTokens: 3,
});
Step 2 - Token Restoration
Token bucket by itself is just that - a storage of tokens. As you burn through the initial tokens, you may want to start restoring some tokens back! This is what token restorers are for.
import {
ContinuousTokenRestorer,
PeriodicTokenRestorer
} from '@mangosteen/rate-limiter';
const periodicRestorer = new PeriodicTokenRestorer({
rate: {
amount: 3,
intervalMs: 1000,
},
});
bucket.addTokenRestorer(periodicRestorer);
const continuousRestorer = new ContinuousTokenRestorer({
rate: {
amount: 3,
intervalMs: 1000,
},
});
bucket.addTokenRestorer(continuousRestorer);
bucket.addTokenRestorer(periodicRestorer1);
bucket.addTokenRestorer(periodicRestorer2);
bucket.addTokenRestorer(periodicRestorer3);
bucket.addTokenRestorer(continuousRestorer1);
bucket.addTokenRestorer(continuousRestorer2);
Step 3.a - Using buckets directly
You can use buckets directly, if you do not care about job ordering, prioritization, or staggering.
const tokenCount = 3.7;
const tokensConsumed: boolean = bucket.consumeTokens(tokenCount);
const { promise, cancel } = bucket.onceTokensAvailable(tokenCount);
await promise;
const { promise, cancel } = bucket.consumeTokensAsync(tokenCount);
await promise;
Step 3.b - Using rate limiter
Using buckets directly has the disadvantage of multiple jobs trying to consume tokens at the same time, with undefined outcome.
If you are spawning lots of jobs constantly, some jobs might get unlucky and never consume any tokens, effectively becoming stuck. This is the same issue that limiter package has.
For that reason, we have created PrioritizedFifoRateLimiter
!
import { PrioritizedFifoRateLimiter } from '@mangosteen/rate-limiter';
const limiter = new PrioritizedFifoRateLimiter({
tokenBucket: bucket,
minStaggerTime: 250,
});
const tokenCount = 5.9;
const priority = 33.33;
const { promise, cancel } = limiter.consumeTokensAsync(tokenCount, priority);
await promise;
Using custom high-precision timer
import {
IntervalScheduler,
TimeoutScheduler,
TokenBucket,
RollingWindowTokenBucket,
PeriodicTokenRestorer
} from '@mangosteen/rate-limiter';
import NanoTimer from 'nanotimer';
const scheduler: TimeoutScheduler<NanoTimer> & IntervalScheduler<NanoTimer> = {
setTimeout: (callback, ms) => {
const timer = new NanoTimer();
timer.setTimeout(callback, [], `${ms}m`);
return timer;
},
clearTimeout: timer => {
timer.clearTimeout();
},
setInterval: (callback, ms) => {
const timer = new NanoTimer();
timer.setInterval(callback, [], `${ms}m`);
return timer;
},
clearInterval: timer => {
timer.clearInterval();
},
};
const bucket1 = new TokenBucket({
initialTokens: 0,
scheduler,
});
const bucket2 = new RollingWindowTokenBucket({
initialTokens: 0,
historyIntervalMs: 1000,
maxHistoryTokens: 10,
scheduler,
});
const periodicRestorer = new PeriodicTokenRestorer({
rate: {
amount: 1,
intervalMs: 1000,
},
scheduler,
});
const limiter = new PrioritizedFifoRateLimiter({
tokenBucket: bucket2,
scheduler,
});