---
title: "@playstack/rate-limit"
description: "Workers-compatible token buckets, private subject keys, and an atomic multi-bucket storage contract."
tags: ["package","operations","rate-limiting","security","free"]
---

{/* package-access:start */}

> **Free.** MIT licensed. Check preview availability before installing. See [package access](/docs/packages#access-policy).

{/* package-access:end */}

`@playstack/rate-limit` evaluates Workers-compatible token-bucket, fixed-window or
rolling-window/cooldown policies with private subject keys and atomic storage.

{/* package-install:start */}

## Install

After confirming [preview access](/docs/packages#access-policy), install the package at your application's shared Playstack version:

```sh
npm install --save-exact @playstack/rate-limit@0.1.0-beta.1
```

Check the peer requirements below before choosing a runtime or provider.

{/* package-install:end */}

## Install and define policy

```ts
import { HmacSha256KeyHasher, createRateLimiter, defineRateLimits } from '@playstack/rate-limit'

const definitions = defineRateLimits({
  'api.request': {
    failureMode: 'closed',
    buckets: [
      { subject: 'account', capacity: 1_000, refill: { tokens: 1_000, everyMs: 60_000 } },
      { subject: 'apiKey', capacity: 100, refill: { tokens: 100, everyMs: 60_000 } },
    ],
  },
})

export const limiter = createRateLimiter({
  definitions,
  environment: 'production',
  store,
  clock,
  keyHasher: new HmacSha256KeyHasher(crypto.subtle, rateLimitKey),
  onEvent: (event) => operations.record(event),
})

const decision = await limiter.consume('api.request', {
  subjects: { account: accountId, apiKey: key.id },
})
```

Raw subject values are HMAC-protected before storage. Use a deployment secret of at least 32 bytes and plan its rotation as a deliberate bucket reset.

## Definition properties

| Property                    | Meaning                                                           |
| --------------------------- | ----------------------------------------------------------------- |
| Definition name             | Lowercase dot notation such as `api.request`.                     |
| `failureMode`               | Explicit `closed` or `open` behavior when storage is unavailable. |
| `buckets`                   | One or more unique subject policies evaluated atomically.         |
| `subject`                   | `account`, `accountMember`, `apiKey`, `custom`, `ip`, or `user`.  |
| `capacity`                  | Maximum burst capacity in tokens.                                 |
| `refill.tokens` / `everyMs` | Positive refill amount and interval.                              |
| `cost`                      | Optional default operation cost; defaults to one token.           |

Values compile to integer microtokens, allowing fractional token policies without floating-point storage drift.

## Preserve fixed-window behavior

Applications deliberately selecting epoch-aligned fixed windows can use `defineFixedWindowRateLimits()` and `createFixedWindowRateLimiter()` from `@playstack/rate-limit/fixed-window`. This is not equivalent to every existing throttler or cooldown policy.

Fixed windows align to epoch boundaries, block until the next boundary after the limit is spent, and avoid partial spending across multi-subject policies. Choosing the strategy in composition keeps the behavior visible; it is not a hidden option that changes token-bucket semantics.

## Rolling expiry and denial cooldown

```ts
import {
  createRollingWindowRateLimiter,
  defineRollingWindowRateLimits,
} from '@playstack/rate-limit/rolling-window'

const limiter = createRollingWindowRateLimiter({
  definitions: defineRollingWindowRateLimits({
    'api.request': {
      failureMode: 'closed',
      windows: [{ subject: 'ip', limit: 10, windowMs: 60_000, cooldownMs: 60_000 }],
    },
  }),
  environment: 'production', store, clock, keyHasher,
})
```

Each accepted hit expires individually. First refusal starts the full cooldown;
blocked attempts neither spend quota nor extend cooldown. Cooldown expiry starts
empty. All subjects are evaluated atomically: denial spends no peer quota and only
exhausted subjects enter cooldown. Limits are bounded at 10,000 per subject.

Use the PostgreSQL `PrismaRollingWindowRateLimitStore` for distributed enforcement;
the memory store is for tests/local single-process use. Rolling Redis and Durable
Object adapters are not supplied. Existing token-bucket/fixed-window defaults are
unchanged. Storage failure remains unavailable under closed policy, not a 429;
fail-open is an explicit bypass. Do not retry an ambiguous commit.

The host owns proxy/subject policy, health exemptions, retry headers and a reviewed
counter cutover. New `psrl:rw:v1:` keys cannot silently share old counters without
changing quota. [PostgreSQL setup](/docs/packages/events-operations/rate-limit-prisma#rolling-window-store)
describes server-clock authority and bounded cleanup.

## Limiter configuration and consumption

| `RateLimiterOptions` property | Required | Purpose                                                        |
| ----------------------------- | -------- | -------------------------------------------------------------- |
| `definitions`                 | Yes      | Validated named policies.                                      |
| `environment`                 | Yes      | Lowercase identifier included in storage-key isolation.        |
| `store`                       | Yes      | Atomic multi-bucket bridge.                                    |
| `clock`                       | Yes      | Application time for stores that do not provide server time.   |
| `keyHasher`                   | Yes      | One-way subject-key derivation.                                |
| `onEvent`                     | No       | Isolated observer for denied or fail-open-bypassed operations. |

`consume` supplies required subject IDs, optional operation cost multiplier, and optional per-subject capacity/refill/cost overrides. Every subject declared by the policy must be resolved before calling it.

## Storage bridge

```ts
interface RateLimitStore {
  name: string
  consume(input: { nowMs: number; buckets: readonly CompiledBucket[] }): Promise<RateLimitDecision>
}
```

The store must decide and update every bucket atomically. A denied group spends none of them. The decision returns overall and per-bucket limit, remaining capacity, and optional retry time.

Use `@playstack/rate-limit/testing` only for tests and local single-process development. Select Redis, Prisma, or Durable Objects at a distributed deployment boundary.

## Boundary

Rate limiting is not a billing meter, entitlement engine, concurrency semaphore, or CDN defense. Resolve authenticated caller and tenant scope first; use edge/CDN defenses independently.

{/* package-reference:start */}

## API entry points and requirements

Reference snapshot: `@playstack/rate-limit@0.1.0-beta.1`. Import only the entry point your runtime needs. Paths below are relative to the installed package; use **Go to Definition** in your editor to inspect exact parameters, return types and overloads. Do not import the declaration-file paths directly.

| Public entry point | Declaration file |
| --- | --- |
| `@playstack/rate-limit/rolling-window` | `./dist/rolling-window.d.ts` |
| `@playstack/rate-limit` | `./dist/index.d.ts` |
| `@playstack/rate-limit/errors` | `./dist/errors.d.ts` |
| `@playstack/rate-limit/fixed-window` | `./dist/fixed-window.d.ts` |
| `@playstack/rate-limit/algorithm` | `./dist/algorithm.d.ts` |
| `@playstack/rate-limit/definitions` | `./dist/definitions.d.ts` |
| `@playstack/rate-limit/types` | `./dist/types.d.ts` |
| `@playstack/rate-limit/testing` | `./dist/testing.d.ts` |
| `@playstack/rate-limit/package.json` | No TypeScript declaration (asset or metadata export). |

Node.js engine requirement: `>=20`. This is not a claim that every entry point works in browsers or Workers.

### Peer dependencies

This package declares no peer dependencies. Its ordinary dependencies are resolved by the package manager.

For a complete first program, start with [Getting started](/docs/getting-started). For API lookup and partial-example conventions, see [Reading the reference](/docs/packages#reading-the-reference). Provider failures, lifecycle requirements and application responsibilities remain described in the guide above; types alone do not establish production safety.

{/* package-reference:end */}
