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 arateLimit
, that per-call limit is used and any plugindefault
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:
- Use
input.rateLimit
if present (per-call override). - Else, use the plugin
default
if configured during setup. - 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 anamespace
skip rate limiting entirely.
Settings
You can configure the plugin either via a shorthand or with full defaults.
Option | Type | Default | Description |
---|---|---|---|
limit | number | — | Shorthand: fixed window limit. Requires duration . |
duration | `string | number` | — |
default | RateLimitRequest | undefined | Default rate-limit applied when a call omits rateLimit . |
identify | `(i: VerifyOptions) => string | null` | identifier ?? ip ?? key |
reason | string | "rate_limited" | Reject reason when limit exceeded. |
analyticsKind | string | undefined | Overrides 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:
Field | Type | Required | Description |
---|---|---|---|
identifier | string | No | Overrides identifier used for limiting. |
namespace | string | Yes (to enable) | Namespaces counters; required when using rate limits. |
rateLimit | RateLimitRequest | No | Per-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 customreason
). - 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.