Skip to main content

Rate Limiting

Most if not all tools we want to connect with restrict the number of calls their API serves using some form of rate limiting.

RateLimiter interface

No two providers implement rate limits the same way. Some use a fixed amount of requests per second, others deduct the weight of your query from a budget, etc. To accommodate for all of these use cases, our Provider class accepts a flexible RateLimiter interface that should provide you with everything you need to achieve your goals.

export type RateLimiter = <T>(
options: { credentials: Credentials; logger: Logger },
targetFunction: () => Promise<Response<T>>,
) => Promise<Response<T>>;

When an instance of Provider class is given a RateLimiter, it will proxy all requests in the form of a targetFunction. This gives the RateLimiter access to the credentials and logger from the RequestOptions, as well as an opportunity to capture the ProviderResponse, which will contain the response's status code, headers and body.

Example

Here's an example of a rate limiting implementation in the case of a provider giving a variable weight to requests depending on their size, and returning a limit-left field in its response body to indicate how many request points are remaining for the credentials used for a given time window.

import {
Cache,
Credentials,
RateLimiter,
ProviderResponse,
HttpErrors
} from '@unito/integration-sdk';
import { createHash } from 'node:crypto';

// Cache shown here for simplicity. Centralize to reuse elsewhere in your integration as needed.
const cache = Cache.create();

export const rateLimiter: RateLimiter = async <T>(
options: { credentials: Credentials },
targetFunction: () => Promise<ProviderResponse<T>>,
): Promise<ProviderResponse<T>> => {
const { apiKey } = getCredentials(options);

// Take care of hashing sensible information before using it as a cache key or value, we don't want to risk a leak!
const remainingLimitCacheKey = `rateLimiter:remainingLimit:${createHash('shake256').update(apiKey).digest('hex')}`;

const remainingLimit: number | undefined = await cache.getValue(remainingLimitCacheKey);

if (typeof remainingLimit === 'number' && remainingLimit <= 0) {
const remainingLimitTime = await cache.getTtl(remainingLimitCacheKey);
throw new HttpErrors.RateLimitExceededError(`Rate limit exceeded. Retry in ${remainingLimitTime} seconds.`);
}

// This provider response is expected to have a 'limit-left' and time-until-reset property to update the remaining
// rate limit, in addition to any other payload you might have been fetching.
const response =
await (<Promise<ProviderResponse<{ 'limit-left'?: number, 'time-until-reset?': number }>>>targetFunction());
const { 'limit-left': newRemainingLimit, 'time-until-reset?': ttl } = response.body;

if (typeof newRemainingLimit === 'number') {
// We could save the new value with ttl equal to time-until-reset (seconds) so we call the provider again
// without worry once the rate limit is reset.
await cache.setValue(remainingLimitCacheKey, newRemainingLimit, ttl);
}

return response;
};

Then, using it in a Provider instance is as easy as passing it in the options upon creation.

import { Provider } from '@unito/integration-sdk';
import { rateLimiter } from './rateLimiter.js;

export const provider = new Provider({
prepareRequest: () => {
return { url: 'https://myProviderUrl.com', headers: {} };
},
rateLimiter,
});

And now every call you make using this Provider instance will use this rateLimiter to make calls to the underlying provider's API!