Concepts
Rate Limiting
A simple guide to rate limiting and how to use it with UsefulKey.
What is rate limiting?
Rate limiting controls how many requests a client can make in a period of time. It protects your API from abuse, reduces accidental overload, and helps enforce fair use across users or IPs.
Common examples:
- Limit each IP to 100 requests per minute
- Limit each user to 1 upload every 5 seconds
- Limit a specific API key to 1,000 requests per hour
When should I use it?
- To prevent spikes that can slow down or crash your service
- To keep costs under control for public APIs
- To enforce plan limits (free vs. paid)
Core concepts
- Identifier: Who or what is being limited. Often an IP address, user ID, or API key. By default, UsefulKey uses
identifier ?? ip ?? key
(in that order). You can override it per call or via plugin settings. - Namespace: A label to group limits by feature or surface (for example,
"global"
,"api"
,"auth"
,"uploads"
). Rate limits are tracked per(namespace, identifier)
. If you omitnamespace
, no rate limiting runs for that call. - Strategy: The algorithm used to enforce limits. UsefulKey supports fixed window, token bucket, and sliding window.
- Precedence: On each verification, UsefulKey applies at most one limit:
- If the call passes a
rateLimit
, it is used. - Else, if the plugin has a
default
, it is used. - Else, rate limiting is skipped.
- If the call passes a
Strategies
- Fixed window: Allow N requests in a time window (for example, 100 per minute). Simple and predictable.
- Token bucket: Start with a capacity, refill tokens over time, and consume per request. Smooths bursts while keeping an average rate.
- Sliding window: Measure usage over a rolling window instead of fixed reset points.
Using rate limiting with UsefulKey
UsefulKey provides rate limiting via the rate limit plugin. You can set a default in configuration and optionally override it per call.
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 notnull
. - 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
- Plugin reference: Rate limit plugin
- Adapter reference: Rate limit store adapters