From bc3e671a908ad75d4ef9fae7a313959ebe32a040 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Sat, 12 Sep 2026 16:24:09 +0700 Subject: [PATCH] feat(ratelimit): accept Redis cluster clients and restructure adapter docs RedisRateLimiter now accepts a node-redis cluster client alongside the standalone client. The Lua script keys on a single key, so EVALSHA routes to the owning shard, and node-redis 6.2+ loads the script on every node. The rate limit docs page now leads with the adapter table in Basic Usage, keeps Blocking Mode with it, and moves Adapters to the end with one subsection and a short description per adapter instead of tabs. --- apps/content/docs/helpers/ratelimit.mdx | 256 ++++++++++++----------- packages/ratelimit/src/adapters/redis.ts | 6 +- 2 files changed, 139 insertions(+), 123 deletions(-) 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