# RateLimiter

`RateLimiter` consumes tokens for a string **key** using either a **fixed-window**
counter or a **token-bucket**, backed by a shared [`RateLimiterStore`](#ratelimiterstore).
It is the building block for protecting external APIs, enforcing per-user or
per-tenant quotas, throttling job workers, and coordinating limits across many
fibers — or many processes when they share the Redis-backed store.

```ts
import { RateLimiter } from "effect/unstable/persistence"
```

A single `consume` call decides whether the request is allowed and tells you what
to do when the limit is exceeded. You pick the behavior with `onExceeded`:

- `"fail"` (the default) raises a [`RateLimiterError`](#ratelimitererror) whose
  `reason` is a [`RateLimitExceeded`](#ratelimitexceeded).
- `"delay"` never fails for overflow; it returns a [`ConsumeResult`](#consumeresult)
  whose `delay` you are expected to honor yourself — or you let the accessors
  ([`makeWithRateLimiter`](#makewithratelimiter) / [`makeSleep`](#makesleep)) wait
  for you.
**Time and windows:** Time is read from the Effect `Clock`, so rate limiters are deterministic under
`TestClock`. The `window` is clamped to at least `1ms`, and refill math uses
millisecond granularity.

## Throttling an effect

The most common case: provide a store and the `RateLimiter` layer, then grab a
`withLimiter` function that wraps any effect so it is throttled. With
`onExceeded: "delay"` the wrapped effect is automatically delayed instead of
failing.

```ts
import { Effect } from "effect"
import { RateLimiter } from "effect/unstable/persistence"

const program = Effect.gen(function* () {
  // Access the `withLimiter` function from the RateLimiter module
  const withLimiter = yield* RateLimiter.makeWithRateLimiter

  // Apply a rate limiter to an effect
  yield* Effect.log("Making a request with rate limiting").pipe(
    withLimiter({
      key: "some-key",
      limit: 10,
      onExceeded: "delay",
      window: "5 seconds",
      algorithm: "fixed-window"
    })
  )
})

// Provide an in-memory store + the RateLimiter service
program.pipe(
  Effect.provide(RateLimiter.layer),
  Effect.provide(RateLimiter.layerStoreMemory),
  Effect.runFork
)
```

`makeWithRateLimiter` calls `consume` with your options, and if the returned
`delay` is non-zero it wraps the effect with `Effect.delay`. If the `delay` is
zero the effect runs immediately.

## Sleeping until allowed

When you do not want to wrap an arbitrary effect — you just want to pause the
current fiber until the next token is available — use [`makeSleep`](#makesleep).
It consumes with `onExceeded: "delay"`, sleeps for the returned delay (only if
non-zero), and yields the full `ConsumeResult`.

```ts
import { Effect } from "effect"
import { RateLimiter } from "effect/unstable/persistence"

const program = Effect.gen(function* () {
  const sleep = yield* RateLimiter.makeSleep

  // Only sleeps if the limit has been exceeded; returns ConsumeResult
  const result = yield* sleep({
    key: "some-key",
    limit: 10,
    window: "5 seconds",
    algorithm: "fixed-window"
  })

  yield* Effect.log(`remaining=${result.remaining}`)
})
```

## Failing fast vs delaying

The same `consume` call behaves very differently depending on `onExceeded`. Fail
fast when you want to reject the caller (e.g. return `429 Too Many Requests`);
delay when you want to smooth traffic and back off.

```ts
import { Effect } from "effect"
import { RateLimiter } from "effect/unstable/persistence"

const failFast = Effect.gen(function* () {
  const limiter = yield* RateLimiter.RateLimiter

  // Raises RateLimiterError (reason: RateLimitExceeded) once the budget is gone
  return yield* limiter.consume({
    key: "user-42",
    limit: 5,
    window: "1 minute",
    onExceeded: "fail"
  })
}).pipe(
  Effect.catchTag("RateLimiterError", (error) =>
    // error.reason is RateLimitExceeded | RateLimitStoreError
    Effect.logWarning(`blocked: ${error.message}`)
  )
)

const computeDelay = Effect.gen(function* () {
  const limiter = yield* RateLimiter.RateLimiter

  // Never fails for overflow — returns the delay YOU must honor
  const result = yield* limiter.consume({
    key: "user-42",
    limit: 5,
    window: "1 minute",
    onExceeded: "delay"
  })

  // You are responsible for actually waiting `result.delay`
  yield* Effect.sleep(result.delay)
})
```

<Aside type="caution" title="`onExceeded: 'delay'` does not wait for you">
A bare `consume({ onExceeded: "delay" })` only *reports* the `delay`; it does not
sleep. Either honor `result.delay` yourself, or use
[`makeWithRateLimiter`](#makewithratelimiter) / [`makeSleep`](#makesleep) which do
the waiting.
</Aside>

When `tokens` exceeds `limit` outright, the request can never succeed: `fail`
raises immediately with `retryAfter` equal to the window, and `delay` returns a
result with `remaining: 0` and `delay` equal to the window.

## Algorithms
**fixed-window vs token-bucket:** - **`fixed-window`** (default): a TTL-driven counter per key. Tokens accumulate
  within a window and the count resets when the TTL elapses. Rejected `fail`
  attempts do **not** extend the TTL, and Redis fixed-window keys **expire
  automatically**, so it is naturally self-cleaning.
- **`token-bucket`**: keeps the remaining token count plus the last-refill time
  and refills continuously based on elapsed time. It does **not** use a TTL, so
  high-cardinality dynamic keys may accumulate state — plan an external cleanup
  or a bounded key strategy.

```ts
import { Effect } from "effect"
import { RateLimiter } from "effect/unstable/persistence"

const burstyButSmooth = Effect.gen(function* () {
  const limiter = yield* RateLimiter.RateLimiter

  // Allow bursts up to 100, refilling smoothly over the window
  return yield* limiter.consume({
    key: "tenant-acme",
    limit: 100,
    window: "1 minute",
    algorithm: "token-bucket",
    onExceeded: "delay"
  })
})
```

## Cross-process coordination

The in-memory store ([`layerStoreMemory`](#layerstorememory)) is **process-local**:
it lives in plain `Map`s and only coordinates fibers inside one runtime. To
enforce a limit across multiple processes or machines, use the Redis-backed store
([`layerStoreRedis`](#layerstoreredis)), which performs all counter updates inside
atomic Lua scripts so concurrent consumers cannot race.

```ts
import { Effect, Layer } from "effect"
import { RateLimiter, Redis } from "effect/unstable/persistence"

// `Redis.Redis` is built from a `send` transport you supply (see the Redis page).
declare const RedisLive: Layer.Layer<Redis.Redis>

const program = Effect.gen(function* () {
  const withLimiter = yield* RateLimiter.makeWithRateLimiter
  yield* Effect.log("shared across processes").pipe(
    withLimiter({ key: "global", limit: 1000, window: "1 minute" })
  )
}).pipe(
  Effect.provide(RateLimiter.layer),
  // Shared, atomic store coordinated through Redis
  Effect.provide(RateLimiter.layerStoreRedis({ prefix: "ratelimiter:" })),
  Effect.provide(RedisLive)
)
```
**Choose stable keys and prefixes:** Rate-limit keys and Redis prefixes share the persistence namespace. Pick stable,
collision-free values so independent limiters don't clobber each other.

---

# Reference

## Model & service

### RateLimiter (interface)

The service shape: a single `consume` method that takes a `key`, a `limit`, a
`window`, and optional `algorithm` / `onExceeded` / `tokens`, returning a
`ConsumeResult` or failing with `RateLimiterError`.

```ts
import { Effect } from "effect"
import { RateLimiter } from "effect/unstable/persistence"

declare const limiter: RateLimiter.RateLimiter

const result = limiter.consume({
  key: "k",
  limit: 10,
  window: "1 minute",
  algorithm: "fixed-window", // default; or "token-bucket"
  onExceeded: "fail", // default; or "delay"
  tokens: 1 // default; how many tokens this call consumes
})
// => Effect.Effect<RateLimiter.ConsumeResult, RateLimiter.RateLimiterError>
```

### RateLimiter (service tag)

The `Context.Service` tag used to access or provide the limiter. Yield it in a
generator to get the live `RateLimiter`.

```ts
import { Effect } from "effect"
import { RateLimiter } from "effect/unstable/persistence"

const use = Effect.gen(function* () {
  const limiter = yield* RateLimiter.RateLimiter
  // => RateLimiter (requires RateLimiter in the environment)
  return limiter
})
```

### ConsumeResult (interface)

Metadata returned after a `consume`:

- `delay` — `Duration` to wait before the next request under `onExceeded: "delay"`;
  `Duration.zero` when the request is allowed immediately.
- `limit` — the configured maximum for the window.
- `remaining` — remaining requests in the current window (can be negative for
  token-bucket overflow).
- `resetAfter` — time until the limit fully resets.

```ts
import { Duration } from "effect"
import type { RateLimiter } from "effect/unstable/persistence"

const allowed: RateLimiter.ConsumeResult = {
  delay: Duration.zero,
  limit: 10,
  remaining: 9,
  resetAfter: Duration.seconds(60)
}
// => { delay: 0ms, limit: 10, remaining: 9, resetAfter: 60s }
```

## Constructors & accessors

### make

An `Effect` that builds a `RateLimiter` from the current `RateLimiterStore`. Use
it when you want the limiter value directly rather than via the layer.

```ts
import { Effect } from "effect"
import { RateLimiter } from "effect/unstable/persistence"

const program = Effect.gen(function* () {
  const limiter = yield* RateLimiter.make // requires RateLimiterStore
  return yield* limiter.consume({ key: "k", limit: 5, window: "10 seconds" })
}).pipe(Effect.provide(RateLimiter.layerStoreMemory))
// => Effect<ConsumeResult, RateLimiterError>
```

### layer

A `Layer` providing `RateLimiter` from a `RateLimiterStore`. Compose it with a
store layer to wire up the service.

```ts
import { Layer } from "effect"
import { RateLimiter } from "effect/unstable/persistence"

const Live = RateLimiter.layer.pipe(
  Layer.provide(RateLimiter.layerStoreMemory)
)
// => Layer<RateLimiter, never, never>
```

### makeWithRateLimiter

An `Effect` yielding a `withLimiter(options)` function that returns an effect
transformer. The transformer delays the wrapped effect by `ConsumeResult.delay`
(or runs it immediately when the delay is zero).

```ts
import { Effect } from "effect"
import { RateLimiter } from "effect/unstable/persistence"

const program = Effect.gen(function* () {
  const withLimiter = yield* RateLimiter.makeWithRateLimiter
  return yield* Effect.succeed("work").pipe(
    withLimiter({ key: "k", limit: 10, window: "1 minute", onExceeded: "delay" })
  )
})
// => the effect, automatically delayed when over the limit
```

### makeSleep

An `Effect` yielding a `sleep(options)` function. It consumes with
`onExceeded: "delay"`, sleeps for the returned delay when non-zero, and yields the
`ConsumeResult`. (Note: `sleep` has no `onExceeded` option — it is always `"delay"`.)

```ts
import { Effect } from "effect"
import { RateLimiter } from "effect/unstable/persistence"

const program = Effect.gen(function* () {
  const sleep = yield* RateLimiter.makeSleep
  const result = yield* sleep({ key: "k", limit: 10, window: "1 minute" })
  return result.remaining
})
// => sleeps only when needed, then returns ConsumeResult
```

## Errors

### RateLimiterError

The top-level error (a `Schema.ErrorClass`) raised by limiter operations. Its
`_tag` is `"RateLimiterError"`, it carries a `reason`
([`RateLimiterErrorReason`](#ratelimitererrorreason)), and its `message` proxies
`reason.message`.

```ts
import { Effect } from "effect"
import { RateLimiter } from "effect/unstable/persistence"

const handled = Effect.gen(function* () {
  const limiter = yield* RateLimiter.RateLimiter
  return yield* limiter.consume({ key: "k", limit: 1, window: "1 minute" })
}).pipe(
  Effect.catchTag("RateLimiterError", (error) => {
    // error._tag === "RateLimiterError"
    // error.reason is RateLimitExceeded | RateLimitStoreError
    return Effect.logWarning(error.message)
  })
)
```

### RateLimitExceeded

The `reason` raised when a `fail` request is over budget (a `Schema.ErrorClass`,
`_tag` `"RateLimitExceeded"`). Fields: `retryAfter` (a `Duration`, encoded as
`DurationFromMillis`), `key`, `limit`, and `remaining`. Its `message` is
`"Rate limit exceeded"`.

```ts
import { Duration } from "effect"
import { RateLimiter } from "effect/unstable/persistence"

const reason = new RateLimiter.RateLimitExceeded({
  key: "user-42",
  retryAfter: Duration.seconds(30),
  limit: 5,
  remaining: 0
})
// => reason._tag === "RateLimitExceeded"; reason.message === "Rate limit exceeded"
```

### RateLimitStoreError

The `reason` raised when the backing store fails (a `Schema.ErrorClass`, `_tag`
`"RateLimitStoreError"`). Fields: `message` and an optional `cause`. The Redis
store maps transport failures into this reason.

```ts
import { RateLimiter } from "effect/unstable/persistence"

const reason = new RateLimiter.RateLimitStoreError({
  message: "Failed to execute fixedWindow rate limiting command"
})
// => reason._tag === "RateLimitStoreError"
```

### RateLimiterErrorReason

The union type `RateLimitExceeded | RateLimitStoreError`, plus an exported
`Schema.Union` value of the same name for decoding/encoding error reasons.

```ts
import { Schema } from "effect"
import { RateLimiter } from "effect/unstable/persistence"

// Value: Schema.Union([RateLimitExceeded, RateLimitStoreError])
const decode = Schema.decodeUnknownSync(RateLimiter.RateLimiterErrorReason)
// type RateLimiterErrorReason = RateLimitExceeded | RateLimitStoreError
```

## Type IDs

### TypeId

The runtime brand for `RateLimiter` values: `"~effect/persistence/RateLimiter"`.
Available both as a value and as a type.

```ts
import { RateLimiter } from "effect/unstable/persistence"

RateLimiter.TypeId // => "~effect/persistence/RateLimiter"
```

### ErrorTypeId

The runtime brand stamped on `RateLimiterError` instances:
`"~@effect/experimental/RateLimiter/RateLimiterError"`. Available as a value and a
type.

```ts
import { RateLimiter } from "effect/unstable/persistence"

RateLimiter.ErrorTypeId // => "~@effect/experimental/RateLimiter/RateLimiterError"
```

## Store

### RateLimiterStore

The low-level `Context.Service` that the limiter delegates to. You normally use a
provided store layer rather than implementing this yourself, but the contract is:

- `fixedWindow({ key, tokens, refillRate, limit })` returns
  `[count, ttl]` — the token `count` *after* taking `tokens`, and the remaining
  TTL in milliseconds. If `limit` is provided and exceeded, the returned `count`
  is greater than the limit **and the TTL is not updated**.
- `tokenBucket({ key, tokens, limit, refillRate, allowOverflow })` returns the
  remaining token count *after* consuming `tokens`. With `allowOverflow: true`
  the count may drop below zero; otherwise a negative return only signals that the
  request exceeded availability — the persisted count is never pushed below zero.

```ts
import { Effect, Duration } from "effect"
import { RateLimiter } from "effect/unstable/persistence"

const probe = Effect.gen(function* () {
  const store = yield* RateLimiter.RateLimiterStore
  const [count, ttl] = yield* store.fixedWindow({
    key: "k",
    tokens: 1,
    refillRate: Duration.seconds(6),
    limit: 10
  })
  return { count, ttl } // => e.g. { count: 1, ttl: 6000 }
}).pipe(Effect.provide(RateLimiter.layerStoreMemory))
```

### layerStoreMemory

A `Layer` providing a process-local `RateLimiterStore` backed by in-memory `Map`s
(one for fixed-window counters, one for token buckets). Ideal for single-process
apps and tests; not shared across processes.

```ts
import { RateLimiter } from "effect/unstable/persistence"

RateLimiter.layerStoreMemory
// => Layer<RateLimiterStore, never, never>
```

### makeStoreRedis

An `Effect` (requiring `Redis.Redis`) that builds a Redis-backed
`RateLimiterStore` using atomic Lua scripts. Optional `prefix` is prepended to
every key (default `"ratelimiter:"`).

```ts
import { Effect } from "effect"
import { RateLimiter } from "effect/unstable/persistence"

const program = Effect.gen(function* () {
  const store = yield* RateLimiter.makeStoreRedis({ prefix: "rl:" })
  return store // => RateLimiterStore (requires Redis.Redis)
})
```

### layerStoreRedis

The `Layer` form of `makeStoreRedis`. Provides a Redis-backed `RateLimiterStore`
and requires `Redis.Redis` in its environment.

```ts
import { RateLimiter } from "effect/unstable/persistence"

RateLimiter.layerStoreRedis({ prefix: "ratelimiter:" })
// => Layer<RateLimiterStore, never, Redis.Redis>
```

### layerStoreRedisConfig

Provides the Redis store from a `Config.Wrap` of `{ prefix? }`, so the prefix can
come from configuration. The resulting layer adds `Config.ConfigError` to its
error channel and still requires `Redis.Redis`.

```ts
import { Config } from "effect"
import { RateLimiter } from "effect/unstable/persistence"

const StoreLive = RateLimiter.layerStoreRedisConfig({
  prefix: Config.string("RATE_LIMIT_PREFIX").pipe(Config.withDefault("ratelimiter:"))
})
// => Layer<RateLimiterStore, Config.ConfigError, Redis.Redis>
```