> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ooneex.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Rate Limit

> Throttle requests by key with Redis-backed counters behind a single IRateLimiter interface.

The `@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) implements `IRateLimiter` — 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()` returns `limited`, `remaining`, `total`, and `resetAt` so you can build `Retry-After` headers 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/container` and inject `AppEnv` for zero-config wiring from environment variables.
* **HTTP-aware errors.** Failures throw `RateLimitException`, which carries HTTP `429 Too Many Requests` and a machine-readable `key`.

## How it works

You resolve a limiter and call `check(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.                                  |

The `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.              |

The backend's window algorithm:

| 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`). |

```bash theme={null}
RATE_LIMIT_REDIS_URL=redis://localhost:6379
```

## Usage

Resolve a limiter from the container — it reads its connection details from the environment — then call `check()` on each request you want to throttle.

```typescript theme={null}
import { RedisRateLimiter } from "@ooneex/rate-limit";
import { container } from "@ooneex/container";

const limiter = container.get(RedisRateLimiter);

// Count this request for the client and inspect the result
const result = await limiter.check("ip:192.168.1.1");

result.limited;   // false while under the limit, true once exceeded
result.remaining; // requests left in the current window
result.total;     // the configured limit
result.resetAt;   // Date the window resets
```

Reject limited requests and tell the client when to retry:

```typescript theme={null}
const result = await limiter.check(`user:${userId}`);

if (result.limited) {
  const retryAfter = Math.ceil((result.resetAt.getTime() - Date.now()) / 1000);
  return new Response("Too Many Requests", {
    status: 429,
    headers: { "Retry-After": String(retryAfter) },
  });
}
```

Use `isLimited()` when you only need the boolean, and `reset()` / `getCount()` for inspection and administration:

```typescript theme={null}
// Boolean-only check
if (await limiter.isLimited(`apikey:${apiKey}`)) {
  return forbidden();
}

// Read the current count without consuming quota
const count = await limiter.getCount("ip:192.168.1.1");

// Clear a key (e.g. after a manual unblock)
const wasReset = await limiter.reset("ip:192.168.1.1");
```

You can override connection and namespace settings through constructor options instead of the environment:

```typescript theme={null}
import { RedisRateLimiter } from "@ooneex/rate-limit";

const limiter = new RedisRateLimiter(env, {
  connectionString: "redis://localhost:6379",
  namespace: "api",
});
```

## 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.

```typescript theme={null}
import { decorator } from "@ooneex/rate-limit";
import { RedisRateLimiter } from "@ooneex/rate-limit";

@decorator.rateLimit()
export class ApiRateLimiter extends RedisRateLimiter {}
```

Resolve it from the container and inject it where you guard a route:

```typescript theme={null}
import { inject } from "@ooneex/container";

export class ApiGuard {
  constructor(@inject(ApiRateLimiter) private readonly limiter: ApiRateLimiter) {}

  public async allow(ip: string): Promise<boolean> {
    return !(await this.limiter.isLimited(`ip:${ip}`));
  }
}
```

## Exceptions

The component throws `RateLimitException` 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.                                        |

```typescript theme={null}
import { RateLimitException } from "@ooneex/rate-limit";

try {
  const result = await limiter.check(`user:${userId}`);
  if (result.limited) {
    return tooManyRequests(result);
  }
} catch (error) {
  if (error instanceof RateLimitException) {
    logger.error(`Rate limit error [${error.key}]: ${error.message}`, error.data);
    // Fail open or closed depending on your policy
  } else {
    throw error;
  }
}
```

## Best practices

* **Choose a stable key scheme.** Throttle by `ip:`, `user:`, or `apikey:` prefixes so each identity is limited independently and `reset()` can target one entry.
* **Use `resetAt` for `Retry-After`.** Derive the header from `result.resetAt` so 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 `namespace` per app when several services share one Redis so counters never collide.
* **Resolve from the container.** Prefer `container.get()` / `@inject` over `new` so configuration flows from `AppEnv` and the limiter stays a singleton.
