Skip to content
LogoLogo

@portable/rate-limit

Functions

RateLimit.create

Creates a rate limiter that allows limit requests per period for each key, with the generic cell rate algorithm (GCRA). Allowance comes back continuously, one request every period / limit milliseconds, up to burst.

function create(options: create.Options): Limiter;
OptionTypeDefaultDescription
limitnumberRequests allowed per period, on average.
periodnumberLength of the period in milliseconds.
burstnumberlimitRequests allowed at once after an idle period.
storeStoreRateLimit.memoryStore()Where the limiter keeps one number per key.
import * as RateLimit from '@portable/rate-limit';
 
const limiter = RateLimit.create({ limit: 100, period: 60_000 });
 
export default {
  async fetch(request: Request) {
    const ip = request.headers.get('cf-connecting-ip') ?? 'unknown';
    const result = await limiter.take(ip, { signal: request.signal });
    if (!result.allowed) {
      return new Response(null, {
        status: 429,
        headers: RateLimit.headers(result),
      });
    }
    return new Response('ok', { headers: RateLimit.headers(result) });
  },
};

RateLimit.headers

Returns the RateLimit and RateLimit-Policy fields of draft-ietf-httpapi-ratelimit-headers-11 for a result, and Retry-After when the request was denied. Times are whole seconds, rounded up.

function headers(
  result: Result,
  options: headers.Options = {},
): Record<string, string>;
OptionTypeDefaultDescription
namestring'default'The quota policy name in both fields.
nowanynew Date()The time resetAt is counted from, as a Date or a Temporal.Instant.
import * as RateLimit from '@portable/rate-limit';
 
const result = await limiter.take(ip);
if (!result.allowed) {
  return new Response(null, {
    status: 429,
    headers: RateLimit.headers(result),
  });
}
// RateLimit-Policy: "default";q=100;w=60
// RateLimit: "default";r=0;t=1
// Retry-After: 1

RateLimit.memoryStore

Creates a store that keeps keys in a Map in this isolate or process. An expired key is deleted when it is read, and all expired keys are deleted whenever the map has doubled in size since the last sweep.

function memoryStore(): {
  get(key: string, options?: Store.GetOptions): number | undefined;
  set(key: string, value: number, options: Store.SetOptions): boolean;
};
import * as RateLimit from '@portable/rate-limit';
 
const store = RateLimit.memoryStore();
const perMinute = RateLimit.create({
  limit: 100,
  period: 60_000,
  store,
});

Types

RateLimit.Limiter

The limiter that create returns.

type Limiter = {
  take(key: string, options?: Limiter.TakeOptions): Promise<Result>;
};

RateLimit.Result

The outcome of take. Durations are milliseconds.

type Result = {
  allowed: boolean;
  limit: number;
  period: number;
  remaining: number;
  resetAt: Date;
  retryAfter: number;
};

RateLimit.Store

Where a limiter keeps one number per key. get returns the stored number, or undefined for a missing or expired key. set writes value only if the key still holds expected, and returns whether it wrote. Either method may return a promise.

type Store = {
  get(
    key: string,
    options: Store.GetOptions,
  ): Store.MaybePromise<number | undefined>;
  set(
    key: string,
    value: number,
    options: Store.SetOptions,
  ): Store.MaybePromise<boolean>;
};