@ooneex/rate-limit component throttles requests per key — an IP, a user ID, an API key, anything you can name. The RedisRateLimiter backend implements the IRateLimiter interface — check, isLimited, reset, getCount — backed by Redis counters, so the limit is shared across every instance of your app. Every check returns a structured result with the remaining quota, the total limit, and the exact time the window resets.
Why this component
- One interface.
RedisRateLimiter(Bun’s native Redis client) implementsIRateLimiter— implement the same interface to swap in your own backend without touching callers. - Limit by any key. Pass any string to
check()—ip:1.2.3.4,user:123,apikey:abc— and each key is throttled independently. - Structured results.
check()returnslimited,remaining,total, andresetAtso you can buildRetry-Afterheaders and surface quota to clients. - Distributed by default. Counters live in Redis, so the limit is shared across every instance of your app.
- Container-managed. Both limiters are registered with
@ooneex/containerand injectAppEnvfor zero-config wiring from environment variables. - HTTP-aware errors. Failures throw
RateLimitException, which carries HTTP429 Too Many Requestsand a machine-readablekey.
How it works
You resolve a limiter and callcheck(key) on every request you want to throttle. The backend increments a counter for that key and returns the current state. When limited is true, the caller has exceeded its quota for the current window.
| Method | Purpose |
|---|---|
check(key) | Count this request and return { limited, remaining, total, resetAt }. |
isLimited(key) | Convenience wrapper — runs check() and returns just the limited boolean. |
reset(key) | Clear the counter for a key; returns whether anything was removed. |
getCount(key) | Read the current count without mutating it. |
RateLimitResultType returned by check():
| Field | Type | Meaning |
|---|---|---|
limited | boolean | true once the key has exceeded the allowed requests in the window. |
remaining | number | Requests left before the key is limited (never below 0). |
total | number | The configured limit for the window. |
resetAt | Date | When the current window expires and the counter resets. |
| Backend | Algorithm | Default window | Key namespace |
|---|---|---|---|
RedisRateLimiter | Fixed-window counter — INCR the key, set a TTL on the first request of the window, and compare against the limit. | 120 requests / 60 seconds | ratelimit: |
RedisRateLimiter namespaces every key as <namespace>:<key> (default namespace ratelimit) so multiple apps can share one Redis instance without colliding.
Environment variables
| Variable | Backend | Required | Purpose |
|---|---|---|---|
RATE_LIMIT_REDIS_URL | RedisRateLimiter | Yes (unless connectionString is passed) | Connection string, e.g. redis://localhost:6379. Missing throws RateLimitException (RATE_LIMIT_CONNECTION_FAILED). |
Usage
Resolve a limiter from the container — it reads its connection details from the environment — then callcheck() on each request you want to throttle.
isLimited() when you only need the boolean, and reset() / getCount() for inspection and administration:
Decorator and usage
@decorator.rateLimit()
Registers a rate limiter class with the container. It accepts an optional scope (defaults to singleton). The built-in limiters are already decorated; use it to register a custom limiter that implements IRateLimiter, or to extend a built-in one under your own name.
Exceptions
The component throwsRateLimitException when a backend is misconfigured or an operation fails. It extends the base Exception, carries the HTTP status 429 Too Many Requests, and exposes a machine-readable key, a human-readable message, and a data object.
| Key | When |
|---|---|
RATE_LIMIT_CONNECTION_FAILED | RedisRateLimiter is created without a connectionString or RATE_LIMIT_REDIS_URL. |
RATE_LIMIT_CHECK_FAILED | A check() (or isLimited()) call against the backend fails. |
RATE_LIMIT_RESET_FAILED | A reset() call against the backend fails. |
RATE_LIMIT_COUNT_FAILED | A getCount() call against the backend fails. |
Best practices
- Choose a stable key scheme. Throttle by
ip:,user:, orapikey:prefixes so each identity is limited independently andreset()can target one entry. - Use
resetAtforRetry-After. Derive the header fromresult.resetAtso clients back off for the right amount of time instead of hammering the endpoint. - Prefer
isLimited()for gating,check()for reporting. Use the boolean when you only allow or deny; use the full result when you surface remaining quota to the caller. - Decide fail-open vs fail-closed. Wrap
check()in try/catch and pick a deliberate behavior when Redis is unreachable — rejecting all traffic on a backend outage may be worse than letting it through. - Namespace shared instances. Set a distinct
namespaceper app when several services share one Redis so counters never collide. - Resolve from the container. Prefer
container.get()/@injectovernewso configuration flows fromAppEnvand the limiter stays a singleton.