---
title: "@unikvs/rate-limit"
description: "Middleware that limits operation frequency with a token bucket."
---

## Overview [#overview]

`RateLimit` limits operation frequency with a token bucket, protecting storage from excessive reads and writes.Middleware

Use it to allow bursts while capping the average rate. On exhaustion, choose waiting or an immediate error.

- `name` is `RateLimit`.
- `isOpen` is always `true`.
- The bucket is shared per middleware instance (not per key), consuming 1 token for every operation (`set` / `get` / `stream` / `has` / `delete` / `copy` / `move` / `clear`).
- `encode` / `decode` / `encodeStream` / `decodeStream` pass data through.

Install with:

```package-install
npm install @unikvs/rate-limit
```

## Usage [#usage]

RateLimit implements IMiddleware. Pass it to UniKvs with use().

```ts
import { RateLimit } from "@unikvs/rate-limit";
```

Constructor arguments:

| Prop | Type | Default | Description |
| - | - | - | - |
| `tokensPerInterval?` | `[#rate-limit-options-tokens-per-interval] number` | `1` | Tokens refilled per interval. |
| `intervalMs?` | `[#rate-limit-options-interval-ms] number` | `1000` | Refill interval (ms). |
| `maxTokens?` | `[#rate-limit-options-max-tokens] number` | - | Maximum tokens held. Defaults to max(1, tokensPerInterval). An explicit value below 1 fails construction. |
| `burst?` | `[#rate-limit-options-burst] number` | - | Alias of maxTokens. When both are given, maxTokens wins. An explicit value below 1 fails construction. |
| `strategy?` | `[#rate-limit-options-strategy] RateLimitStrategy` | `"wait"` | Behavior on exhaustion. "wait" waits for refill; "throw" throws immediately. |
| `now?` | `[#rate-limit-options-now] () => number` | `Date.now` | Returns the current time (epoch ms). Defaults to Date.now. |
| `sleep?` | `[#rate-limit-options-sleep] IRateLimitSleep` | - | Wait implementation. Uses setTimeout by default; swap it to control time in tests. |

The following caps usage at 5 operations per second, waiting on exhaustion:

```ts
import { Memory } from "@unikvs/memory";
import { RateLimit } from "@unikvs/rate-limit";
import * as unikvs from "unikvs";

const kvs = new unikvs.Builder<{ k1: unikvs.PlainValue<string> }>()
  .use(new RateLimit({ tokensPerInterval: 5, intervalMs: 1000 }))
  .persist(new Memory())
  .build();

await kvs.open();
await kvs.set("k1", "hello");
console.log(await kvs.get("k1")); // "hello"
await kvs.close();
```

### `RateLimit` [#rate-limit]

Middleware that limits operation frequency with a token bucket. It extends `PassThrough` and performs no data conversion.

### `RateLimitOptions` [#rate-limit-options]

Options accepted by the `RateLimit` constructor. Specifies token counts, refill interval, and exhaustion behavior.

### `RateLimitStrategy` [#rate-limit-strategy]

Behavior on exhaustion. `"wait"` waits until tokens refill; `"throw"` throws immediately.

### `IRateLimitSleep` [#irate-limit-sleep]

Wait implementation. Swap it to control time in tests.

## Data types [#data-types]

| Method         | Input | Output |
| -------------- | ----- | ------ |
| `encode`       | `any` | `any`  |
| `decode`       | `any` | `any`  |
| `encodeStream` | `any` | `any`  |
| `decodeStream` | `any` | `any`  |

- `use` consumes one token per operation and never touches data.
- `encode` / `decode` / `encodeStream` / `decodeStream` pass input through unchanged.
- Tokens refill with elapsed time and never exceed the cap. A clock moving backward never adds tokens.
- When `maxTokens` is omitted, the capacity is `max(1, tokensPerInterval)`. Since each operation consumes one token, a `tokensPerInterval` below 1 still yields a capacity of 1.
- An explicit `maxTokens` or `burst` below 1 throws `RateLimitInvalidOptionError` at construction. `tokensPerInterval` may be a fractional value below 1.
- The `wait` strategy waits until recovery, serving requests FIFO. It aborts waiting on the abort signal.

## Errors [#errors]

Errors thrown by this package:

### RateLimitExceededError [#error-rate-limit-exceeded]

| Prop | Type | Default | Description |
| - | - | - | - |
| `retryAfterMs` | `[#rate-limit-exceeded-retry-after-ms] number` | - | Time to wait before retrying (ms). |

#### Meaning [#error-rate-limit-exceeded-meaning]

Thrown when an operation arrives with no tokens left and `strategy` is `"throw"`. `next` is not called and the operation never runs.

#### How to fix [#error-rate-limit-exceeded-howto]

Wait `meta.retryAfterMs` before retrying, or use `strategy: "wait"` for automatic waiting. If this happens often, revisit `tokensPerInterval` or `maxTokens`.

#### Example [#error-rate-limit-exceeded-example]

Catching `RateLimitExceededError`:

```ts
import { RateLimitExceededError } from "@unikvs/rate-limit";

try {
  await kvs.set("k1", "hello");
} catch (ex) {
  if (ex instanceof RateLimitExceededError) {
    console.log(ex.meta); // { retryAfterMs: 500 }
  } else {
    throw ex;
  }
}
```

### RateLimitInvalidOptionError [#error-rate-limit-invalid-option]

| Prop | Type | Default | Description |
| - | - | - | - |
| `name` | `[#rate-limit-invalid-option-name] string` | - | The offending option name. |
| `actual` | `[#rate-limit-invalid-option-actual] unknown` | - | The given value. |

#### Meaning [#error-rate-limit-invalid-option-meaning]

Thrown when the constructor receives invalid options: non-positive-finite numbers, an explicit `maxTokens` or `burst` below 1, a `strategy` other than `"wait"` / `"throw"`, or a non-function `now` / `sleep`.

#### How to fix [#error-rate-limit-invalid-option-howto]

Check `meta.name`: pass positive finite numbers, a valid enum value, or functions. When both `maxTokens` and `burst` are given, `maxTokens` wins.

#### Example [#error-rate-limit-invalid-option-example]

Catching `RateLimitInvalidOptionError`:

```ts
import { RateLimit, RateLimitInvalidOptionError } from "@unikvs/rate-limit";

try {
  const limiter = new RateLimit({ intervalMs: 0 });
} catch (ex) {
  if (ex instanceof RateLimitInvalidOptionError) {
    console.log(ex.meta); // { name: "intervalMs", actual: 0 }
  } else {
    throw ex;
  }
}
```

`AbortError` from the abort signal propagates as-is, and this middleware consumes no token in that case.
