UsefulKey
Plugins

Rate limit

Apply rate limits per call or via defaults. Supports fixed window and token bucket.

How it works

  • Applies a rate limit per verification call using an identifier and namespace. If no per-call rateLimit is provided, a plugin default can apply.
  • When exceeded, verification rejects with "rate_limited" (customizable).

Default vs per-call precedence

  • If verifyKey provides a rateLimit, that per-call limit is used and any plugin default is ignored for that call.
  • Otherwise, if the plugin is configured with a default, that default is used.
  • If neither is present, no rate limiting is applied.
  • Limits are not combined or applied cumulatively; exactly one configuration is enforced per call.

Precedence in detail

The plugin evaluates the effective configuration in this order per call:

  1. Use input.rateLimit if present (per-call override).
  2. Else, use the plugin default if configured during setup.
  3. Else, skip rate limiting for that call.

Examples:

// 1) Per-call override wins
await uk.verifyKey({
  key,
  identifier: "ip_1.1.1.1",
  namespace: "api",
  rateLimit: { kind: "fixed", limit: 100, duration: "1m" },
});
// Uses limit:100/1m (ignores any plugin default)

// 2) Falls back to plugin default when per-call not set
await uk.verifyKey({
  key,
  identifier: "ip_1.1.1.1",
  namespace: "api",
});
// Uses the plugin default if configured (e.g., limit:10/30s)

// 3) No limit if neither per-call nor default present
await uk.verifyKey({ key, identifier: "ip_1.1.1.1", namespace: "api" });
// If the plugin was configured without a default, this call is not limited

Note: You must provide a namespace to enable limiting. Calls without a namespace skip rate limiting entirely.

Settings

You can configure the plugin either via a shorthand or with full defaults.

OptionTypeDefaultDescription
limitnumberShorthand: fixed window limit. Requires duration.
duration`stringnumber`
defaultRateLimitRequestundefinedDefault rate-limit applied when a call omits rateLimit.
identify`(i: VerifyOptions) => stringnull`identifier ?? ip ?? key
reasonstring"rate_limited"Reject reason when limit exceeded.
analyticsKindstringundefinedOverrides analytics event kind label (fixed, tokenBucket).

RateLimitRequest shapes:

  • Fixed window: { kind: "fixed", limit: number, duration: string | number }
  • Token bucket: { kind: "tokenBucket", capacity: number, refill: { tokens: number, interval: string | number }, cost?: number }

Per-call options for verify

Alongside the key and optional IP, pass:

FieldTypeRequiredDescription
identifierstringNoOverrides identifier used for limiting.
namespacestringYes (to enable)Namespaces counters; required when using rate limits.
rateLimitRateLimitRequestNoPer-call limit; overrides the plugin default if provided.

Emitted analytics on block: "ratelimit.blocked" with payload { kind, namespace, identifier, reset, limit?, capacity?, remaining, ts }.

Usage

import { usefulkey } from "betterkey";
import { ratelimit } from "usefulkey/plugins/rate-limit/global";

// Optional default (applies when a verify call does not pass rateLimit)
const uk = usefulkey({}, {
  plugins: [
    ratelimit({ default: { kind: "fixed", limit: 10, duration: "30s" } }),
  ],
});

// Per-call override
await uk.verifyKey({
  key,
  identifier: req.ip,
  namespace: "app",
  rateLimit: { kind: "fixed", limit: 100, duration: "1m" },
});

// Note: `namespace` must be provided to enable limiting. Calls without a
// namespace skip rate limiting entirely.

Verification behavior

  • Exceeded → { valid: false, reason: "rate_limited" } (or custom reason).
  • Identifier resolution: identifier ?? ip ?? key unless overridden.

Namespaces

Namespaces partition rate-limit counters. Limits are tracked per (namespace, identifier) pair, so changing either creates an independent counter.

  • Why use namespaces? To isolate rate limits by API surface, plan, or feature. Common choices: "global", "api", "auth", "uploads", or per-tenant values.
  • Storage shape (conceptual): counters are keyed like "<namespace>:<identifier>". For example, "global:ip_1.1.1.1".
  • Omitting namespace: disables rate limiting for that call.
  • Changing namespace: resets the window/bucket for the call because it addresses a different counter.

Identifier defaults to identifier ?? ip ?? key but can be overridden per call (set identifier) or globally via the plugin identify function.