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

# Upstash Rate Limiter

> Serverless, distributed rate limiting backed by Upstash Redis behind the framework's IRateLimiter interface.

`UpstashRedisRateLimiter` is the [Rate Limit](/components/rate-limit) component's serverless backend. It builds on [`@upstash/ratelimit`](https://github.com/upstash/ratelimit) and [Upstash Redis](https://upstash.com) over HTTP REST to enforce distributed limits across instances and edge functions — no shared in-memory state. It implements the framework's `IRateLimiter` interface, so the calling code is identical to any other backend.

## Why Upstash

* **Distributed by default.** Counters live in Upstash Redis, so the same limit holds across every instance and region.
* **Serverless-friendly.** Talks to Upstash over HTTP REST, which works in edge functions and short-lived runtimes.
* **Three algorithms.** Fixed window, sliding window, or token bucket — chosen with a single option.
* **Built-in analytics.** Upstash rate-limit analytics are enabled by default for dashboard insight.
* **Same `IRateLimiter` interface.** `check`, `isLimited`, `reset`, and `getCount` — identical to the other backends.

## Installation

`UpstashRedisRateLimiter` ships with `@ooneex/rate-limit` and depends on the Upstash rate-limit and Redis clients.

```bash theme={null}
bun add @ooneex/rate-limit @upstash/ratelimit @upstash/redis
```

## Environment variables

| Variable                         | Required | Purpose                                                                            |
| -------------------------------- | -------- | ---------------------------------------------------------------------------------- |
| `RATE_LIMIT_UPSTASH_REDIS_URL`   | Yes      | Upstash Redis REST URL. Missing throws `RateLimitException` (`CONFIG_REQUIRED`).   |
| `RATE_LIMIT_UPSTASH_REDIS_TOKEN` | Yes      | Upstash Redis REST token. Missing throws `RateLimitException` (`CONFIG_REQUIRED`). |

```bash theme={null}
RATE_LIMIT_UPSTASH_REDIS_URL=https://your-db.upstash.io
RATE_LIMIT_UPSTASH_REDIS_TOKEN=your_rest_token
```

Both can also be passed through constructor options (`url`, `token`), which take precedence over the environment. They are validated when the limiter is constructed, so misconfiguration fails fast.

## Options

`UpstashRedisRateLimiter` accepts an options object as its second constructor argument:

| Option      | Type                   | Default                          | Purpose                                      |
| ----------- | ---------------------- | -------------------------------- | -------------------------------------------- |
| `url`       | `string`               | `RATE_LIMIT_UPSTASH_REDIS_URL`   | Override the REST URL.                       |
| `token`     | `string`               | `RATE_LIMIT_UPSTASH_REDIS_TOKEN` | Override the REST token.                     |
| `algorithm` | `UpstashAlgorithmType` | sliding window, 120 / 60 s       | The limiting algorithm and its parameters.   |
| `prefix`    | `string`               | `"ratelimit"`                    | Redis key prefix; falls back to `namespace`. |
| `namespace` | `string`               | `"ratelimit"`                    | Alias for `prefix`.                          |
| `analytics` | `boolean`              | `true`                           | Enable Upstash rate-limit analytics.         |

### Algorithms

The `algorithm` option is a tagged union — pick one and supply its parameters:

```typescript theme={null}
// Fixed window: limit requests per fixed time bucket
{ type: "fixedWindow", limit: 100, window: "60 s" }

// Sliding window (default): smoother limit over a rolling window
{ type: "slidingWindow", limit: 120, window: "60 s" }

// Token bucket: refillRate tokens per interval, up to maxTokens
{ type: "tokenBucket", refillRate: 10, interval: "10 s", maxTokens: 100 }
```

`window` and `interval` are Upstash `Duration` strings such as `"60 s"`, `"1 m"`, or `"1 h"`. The default is a sliding window of 120 requests per 60 seconds.

## How it works

`check(key)` calls the underlying limiter and normalizes the result into a `RateLimitResultType`:

| Field       | Type      | Purpose                                  |
| ----------- | --------- | ---------------------------------------- |
| `limited`   | `boolean` | Whether this request exceeded the limit. |
| `remaining` | `number`  | Requests left in the current window.     |
| `total`     | `number`  | The configured limit.                    |
| `resetAt`   | `Date`    | When the window resets.                  |

| Method           | Purpose                                                     |
| ---------------- | ----------------------------------------------------------- |
| `check(key)`     | Consume one unit and return the full result.                |
| `isLimited(key)` | Convenience boolean — whether the key is over the limit.    |
| `reset(key)`     | Reset the used tokens for a key; returns `true` on success. |
| `getCount(key)`  | Remaining requests for a key.                               |

## Usage

Key the limiter by whatever identifies the caller — an IP, a user id, or an API key.

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

const limiter = container.get(UpstashRedisRateLimiter);

const result = await limiter.check(`ip:${ip}`);
if (result.limited) {
  return context.response
    .status(429)
    .header(
      "Retry-After",
      String(Math.ceil((result.resetAt.getTime() - Date.now()) / 1000)),
    )
    .json({ error: "Too many requests", resetAt: result.resetAt });
}

return context.response.json({ remaining: result.remaining });
```

Configure a custom algorithm by subclassing the limiter:

```typescript theme={null}
import { decorator } from "@ooneex/rate-limit";
import { UpstashRedisRateLimiter } from "@ooneex/rate-limit";
import { AppEnv } from "@ooneex/app-env";
import { inject } from "@ooneex/container";

@decorator.rateLimit()
export class LoginRateLimiter extends UpstashRedisRateLimiter {
  constructor(@inject(AppEnv) env: AppEnv) {
    super(env, {
      prefix: "login",
      algorithm: { type: "fixedWindow", limit: 5, window: "1 m" },
    });
  }
}
```

## Use in the app

In an `@ooneex/app` application, pass `UpstashRedisRateLimiter` to the `rateLimiter` slot of your `App` config. The framework registers it and exposes it as the `"rateLimiter"` container constant.

```bash theme={null}
bun add @ooneex/app @ooneex/rate-limit @upstash/ratelimit @upstash/redis
```

```typescript theme={null}
import { App } from "@ooneex/app";
import { UpstashRedisRateLimiter } from "@ooneex/rate-limit";
import { TerminalLogger } from "@ooneex/logger";

const app = new App({
  routing: { prefix: "/api" },
  loggers: [TerminalLogger],
  rateLimiter: UpstashRedisRateLimiter,
});

await app.run();
```

With a rate limiter configured, the framework checks it **before every route**, keyed by the client IP — you don't call `check()` yourself. When a request is over the limit it short-circuits with a `429 Too Many Requests` and sets the standard headers automatically:

```
HTTP/1.1 429 Too Many Requests
Retry-After: 42
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1719571200
```

If the limiter backend errors, the check **fails open** (the request proceeds) and the error is logged, so a transient Upstash outage doesn't take your API down. To customize the algorithm or limits, register a subclass (see [Usage](#usage)) and pass that class to the `rateLimiter` slot instead. Set `RATE_LIMIT_UPSTASH_REDIS_URL` and `RATE_LIMIT_UPSTASH_REDIS_TOKEN` in your `.env.yml` (or environment).

## Exceptions

`UpstashRedisRateLimiter` throws `RateLimitException` (HTTP `429`) on misconfiguration or operation failures, carrying a machine-readable `key`.

| Key                       | When                                                                    |
| ------------------------- | ----------------------------------------------------------------------- |
| `CONFIG_REQUIRED`         | The REST URL or token is missing from both options and the environment. |
| `RATE_LIMIT_CHECK_FAILED` | A `check()` call against Upstash failed.                                |
| `RATE_LIMIT_RESET_FAILED` | A `reset()` call failed.                                                |
| `RATE_LIMIT_COUNT_FAILED` | A `getCount()` call failed.                                             |

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

try {
  const result = await limiter.check(`ip:${ip}`);
} catch (error) {
  if (error instanceof RateLimitException) {
    logger.error(`Rate limit error [${error.key}]: ${error.message}`);
  }
  throw error;
}
```

## Best practices

* **Pick the algorithm for the job.** Sliding window for smooth API limits, fixed window for simple per-minute caps, token bucket for burst-tolerant limits.
* **Key by the right identity.** Rate-limit by user id for authenticated routes and by IP for public ones; use a distinct `prefix` per limiter.
* **Surface `resetAt`.** Return a `Retry-After` header and `429` so clients back off correctly.
* **Keep credentials in the environment.** Load the REST URL and token from `.env`; never hard-code them.
* **Share one Upstash database safely.** Give each limiter its own `prefix`/`namespace` so independent limits don't collide.

See the [Rate Limit component](/components/rate-limit) for the full interface and the other backends.
