diff --git a/apps/content/docs/helpers/ratelimit.mdx b/apps/content/docs/helpers/ratelimit.mdx index 69738ed99..fbb163c45 100644 --- a/apps/content/docs/helpers/ratelimit.mdx +++ b/apps/content/docs/helpers/ratelimit.mdx @@ -13,7 +13,17 @@ npm install @orpc/ratelimit@beta ## Basic Usage -The core concept is the `RateLimiter` interface, which defines a standard way to check and enforce rate limits. You can create your own custom limiter or use one of the provided adapters for popular storage backends. The `limit` method accepts a key and an optional `weight` value, which defaults to `1`, so a single request can consume multiple points. +The core concept is the `RateLimiter` interface, which defines a standard way to check and enforce rate limits. You can create your own custom limiter or use one of the provided [adapters](#adapters): + +| Name | Blocking Mode | Adapter for | +| -------------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------- | +| [`MemoryRateLimiter`](#memory) | ✅ | In-memory storage | +| [`RedisRateLimiter`](#redis) | ✅ | [Redis](https://github.com/redis/redis) | +| [`UpstashRateLimiter`](#upstash) | ✅ | [Upstash Rate Limit](https://www.npmjs.com/package/@upstash/ratelimit) | +| [`BunRedisRateLimiter`](#bun-redis) | ✅ | [Bun's Redis](https://bun.com/docs/runtime/redis) | +| [`CloudflareRateLimiter`](#cloudflare) | ❌ | [Cloudflare's Rate Limiting](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/) | + +The `limit` method accepts a key and an optional `weight` value, which defaults to `1`, so a single request can consume multiple points. ```ts twoslash import { MemoryRateLimiter } from '@orpc/ratelimit/memory' @@ -38,22 +48,114 @@ if (!result.success) { } ``` -## Adapters +### Blocking Mode + +Some adapters support blocking mode, which waits until capacity becomes available instead of rejecting requests immediately. + +```ts +const limiter = new MemoryRateLimiter({ + maxRequests: 10, + window: 60000, + blockingUntilReady: { + enabled: true, // Disabled by default + timeout: 5000, // Wait up to 5 seconds + }, +}) +``` -The package includes adapters for multiple storage backends and runtimes. -Each adapter might require `maxRequests` and `window` to configure the limit, along with adapter specific options. +## Ratelimit Middleware -| Name | Blocking Mode | Adapter for | -| ----------------------- | ------------- | --------------------------------------------------------------------------------------------------------- | -| `MemoryRateLimiter` | ✅ | In-memory storage | -| `RedisRateLimiter` | ✅ | [Redis](https://github.com/redis/redis) | -| `UpstashRateLimiter` | ✅ | [Upstash Rate Limit](https://www.npmjs.com/package/@upstash/ratelimit) | -| `BunRedisRateLimiter` | ✅ | [Bun's Redis](https://bun.com/docs/runtime/redis) | -| `CloudflareRateLimiter` | ❌ | [Cloudflare's Rate Limiting](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/) | +The `ratelimit` helper creates middleware that enforces rate limits for [procedures](/docs/procedure). - +```ts +import { ratelimit, RateLimiter } from '@orpc/ratelimit' -```ts memory +const procedure = os + .$context<{ ratelimiter: RateLimiter }>() + .input(z.object({ email: z.email() })) + .use( + ratelimit({ + limiter: ({ context }) => context.ratelimiter, + key: ({ context }, input) => `login:${input.email}`, + weight: 1, // Optional weight for each request, default is 1 + }), + ) + .handler(({ input }) => { + return { success: true } + }) + +const ratelimiter = new MemoryRateLimiter({ + maxRequests: 10, + window: 60000, +}) + +const result = await call( + procedure, + { email: 'user@example.com' }, + { context: { ratelimiter } } +) +``` + +:::info[Automatic Deduplication] +When the same `limiter` and `key` combination is used multiple times in a single request chain, the `ratelimit` middleware performs the rate limit check only once. This behavior follows the [Dedupe Middleware](/docs/recipes/dedupe-middleware) recipe. To disable deduplication, set `dedupe: false`. +::: + +:::tip[Conditional Limiter] +You can choose different limiters dynamically based on the request context: + +```ts +const premiumLimiter = new MemoryRateLimiter({ + maxRequests: 100, + window: 60000, +}) + +const standardLimiter = new MemoryRateLimiter({ + maxRequests: 10, + window: 60000, +}) + +const result = await call( + procedure, + { email: 'user@example.com' }, + { + context: { + ratelimiter: isPremiumUser ? premiumLimiter : standardLimiter, + }, + }, +) +``` + +::: + +## Handler Plugin + +The `RateLimitHandlerPlugin` automatically adds HTTP rate limiting headers (`RateLimit-*` and `Retry-After`) to responses when used with [Ratelimit Middleware](#ratelimit-middleware). This lets clients inspect the current limit state and know when they can retry after hitting a limit. + +```ts +import { RateLimitHandlerPlugin } from '@orpc/ratelimit' + +const handler = new RPCHandler(router, { + plugins: [ + new RateLimitHandlerPlugin(), + ], +}) +``` + +:::info +You can combine this plugin with [Retry After Plugin](/docs/plugins/retry-after) to enable automatic client-side retries based on server rate limiting headers. +::: + +:::info +The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or a custom one. +::: + +## Adapters + +### Memory + +Keeps counters in the memory of the current process, so limits are not shared between instances. A good fit for development, tests, and single-process servers. + +```ts import { MemoryRateLimiter } from '@orpc/ratelimit/memory' const limiter = new MemoryRateLimiter({ @@ -83,10 +185,15 @@ const limiter = new MemoryRateLimiter({ }) ``` -```ts redis +### Redis + +Stores counters in Redis, so every instance using the same server enforces the same limits. Works with both standalone and cluster clients. + +```ts import { RedisRateLimiter } from '@orpc/ratelimit/redis' import { createClient } from 'redis' +// Both standalone (`createClient`) and cluster (`createCluster`) clients are supported. const client = createClient({ url: 'redis://localhost:6379' }) // RedisRateLimiter lazily connects to Redis when needed. @@ -127,7 +234,11 @@ const limiter = new RedisRateLimiter(client, { }) ``` -````ts upstash +### Upstash + +Delegates to an `@upstash/ratelimit` instance, so the algorithm and limits are configured there. A good fit for serverless and edge runtimes. + +````ts import { Ratelimit } from '@upstash/ratelimit' import { Redis } from '@upstash/redis' import { UpstashRateLimiter } from '@orpc/ratelimit/upstash' @@ -170,7 +281,11 @@ const limiter = new UpstashRateLimiter(ratelimit, { }) ```` -```ts bun +### Bun Redis + +The Redis adapter for Bun's built-in Redis client, with no extra dependency. It shares counters with `RedisRateLimiter`. + +```ts import { BunRedisRateLimiter } from '@orpc/bun' import { redis } from 'bun' @@ -208,7 +323,11 @@ const limiter = new BunRedisRateLimiter(redis, { }) ``` -```ts cloudflare +### Cloudflare + +Uses the Workers Rate Limiting binding, so limits are configured in your Worker settings rather than in the adapter. + +```ts import { CloudflareRateLimiter } from '@orpc/cloudflare' export default { @@ -224,106 +343,3 @@ export default { } } ``` - - - -### Blocking Mode - -Some adapters support blocking mode, which waits until capacity becomes available instead of rejecting requests immediately. - -```ts -const limiter = new MemoryRateLimiter({ - maxRequests: 10, - window: 60000, - blockingUntilReady: { - enabled: true, // Disabled by default - timeout: 5000, // Wait up to 5 seconds - }, -}) -``` - -## Ratelimit Middleware - -The `ratelimit` helper creates middleware that enforces rate limits for [procedures](/docs/procedure). - -```ts -import { ratelimit, RateLimiter } from '@orpc/ratelimit' - -const procedure = os - .$context<{ ratelimiter: RateLimiter }>() - .input(z.object({ email: z.email() })) - .use( - ratelimit({ - limiter: ({ context }) => context.ratelimiter, - key: ({ context }, input) => `login:${input.email}`, - weight: 1, // Optional weight for each request, default is 1 - }), - ) - .handler(({ input }) => { - return { success: true } - }) - -const ratelimiter = new MemoryRateLimiter({ - maxRequests: 10, - window: 60000, -}) - -const result = await call( - procedure, - { email: 'user@example.com' }, - { context: { ratelimiter } } -) -``` - -:::info[Automatic Deduplication] -When the same `limiter` and `key` combination is used multiple times in a single request chain, the `ratelimit` middleware performs the rate limit check only once. This behavior follows the [Dedupe Middleware](/docs/recipes/dedupe-middleware) recipe. To disable deduplication, set `dedupe: false`. -::: - -:::tip[Conditional Limiter] -You can choose different limiters dynamically based on the request context: - -```ts -const premiumLimiter = new MemoryRateLimiter({ - maxRequests: 100, - window: 60000, -}) - -const standardLimiter = new MemoryRateLimiter({ - maxRequests: 10, - window: 60000, -}) - -const result = await call( - procedure, - { email: 'user@example.com' }, - { - context: { - ratelimiter: isPremiumUser ? premiumLimiter : standardLimiter, - }, - }, -) -``` - -::: - -## Handler Plugin - -The `RateLimitHandlerPlugin` automatically adds HTTP rate limiting headers (`RateLimit-*` and `Retry-After`) to responses when used with [Ratelimit Middleware](#ratelimit-middleware). This lets clients inspect the current limit state and know when they can retry after hitting a limit. - -```ts -import { RateLimitHandlerPlugin } from '@orpc/ratelimit' - -const handler = new RPCHandler(router, { - plugins: [ - new RateLimitHandlerPlugin(), - ], -}) -``` - -:::info -You can combine this plugin with [Retry After Plugin](/docs/plugins/retry-after) to enable automatic client-side retries based on server rate limiting headers. -::: - -:::info -The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or a custom one. -::: diff --git a/packages/ratelimit/src/adapters/redis.ts b/packages/ratelimit/src/adapters/redis.ts index 829fc13bc..9e43da043 100644 --- a/packages/ratelimit/src/adapters/redis.ts +++ b/packages/ratelimit/src/adapters/redis.ts @@ -1,4 +1,4 @@ -import type { RedisClientType } from 'redis' +import type { RedisClientType, RedisClusterType } from 'redis' import type { RateLimiter, RateLimitOptions, RateLimitResult } from '../types' import { sleep } from '@orpc/shared' @@ -63,7 +63,7 @@ export interface RedisRateLimiterOptions { * @see {@link https://orpc.dev/docs/helpers/ratelimit#adapters | Rate Limit Helpers - Adapters} */ export class RedisRateLimiter implements RateLimiter { - private readonly redis: RedisClientType + private readonly redis: RedisClientType | RedisClusterType private readonly prefix: string private readonly maxRequests: number private readonly window: number @@ -72,7 +72,7 @@ export class RedisRateLimiter implements RateLimiter { private scriptSha: undefined | Awaited> constructor( - redis: RedisClientType, + redis: RedisClientType | RedisClusterType, options: RedisRateLimiterOptions, ) { this.redis = redis