UsefulKey

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.

Configure a default

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

const uk = usefulkey({}, {
  plugins: [
    ratelimit({
      // Applies when a call does not pass its own rateLimit
      default: { kind: "fixed", limit: 100, duration: "1m" },
    }),
  ],
});

Per-call override

await uk.verifyKey({
  key,
  identifier: req.ip,       // who to limit (override if needed)
  namespace: "api",         // required to enable limiting
  rateLimit: { kind: "tokenBucket", capacity: 120, refill: { tokens: 2, interval: "1s" } },
});

Namespaces example

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

// Configure a default global limit: 100 requests per minute
const uk = usefulkey({}, {
  plugins: [
    ratelimit({ default: { kind: "fixed", limit: 100, duration: "1m" } }),
  ],
});

// 1) Global traffic — counts per (namespace="global", identifier)
await uk.verifyKey({
  key,
  identifier: req.ip,   // who is limited (could be userId or key)
  namespace: "global",  // what is limited (global bucket)
});

// 2) Auth endpoints — their own tighter bucket
await uk.verifyKey({
  key,
  identifier: req.ip,
  namespace: "auth",    // separate counters from "global"
  rateLimit: { kind: "fixed", limit: 10, duration: "1m" }, // per-call override
});

// 3) Uploads — token bucket for smoother bursts
await uk.verifyKey({
  key,
  identifier: userId,    // limit per user
  namespace: "uploads",
  rateLimit: {
    kind: "tokenBucket",
    capacity: 30,
    refill: { tokens: 1, interval: "2s" },
  },
});

// 4) Tenant isolation — each tenant has its own namespace
await uk.verifyKey({
  key,
  identifier: userId,
  namespace: `tenant:${tenantId}`, // independent counters per tenant
});

Notes:

  • Only one configuration is applied per call (they are not combined).
  • A namespace is required; if omitted, rate limiting is skipped.

Choosing a storage backend

Rate limits are persisted using a rate limit store adapter. Choose one that matches your environment:

  • Memory (default): Best for tests or single-process demos.
  • Redis: Great for distributed environments; fast and scalable.
  • Postgres or SQLite: Useful when you prefer SQL or already have a database.

See available adapters: Rate limit store adapters.

Best practices

  • Pick clear namespaces: Separate "api", "auth", "uploads", or per-tenant values to isolate counters.
  • Be consistent with identifiers: Use the same identifier source for comparable calls (for example, always IP or always user ID).
  • Start with conservative defaults: Then relax as you learn real traffic patterns.
  • Handle rejections gracefully: Return a clear error and include the reset time if possible.
  • Monitor and tune: Use analytics to track blocks and adjust limits over time.

Troubleshooting

  • Limits never trigger: Ensure you passed a namespace and that your identifier is not null.
  • Unexpectedly tight or loose limiting: Double-check the strategy and values (for example, duration units, refill rates, or capacity).
  • Different endpoints share limits: Use different namespace values to separate them.

See also