Rate Limit Helpers
Rate Limit helpers provide a unified set of adapters, middleware, and handler plugins for adding rate limiting to oRPC applications. They are flexible and composable, so you can use different rate-limiting strategies and storage backends without changing your procedure code.
Installation
npm install @orpc/ratelimit@betapnpm add @orpc/ratelimit@betayarn add @orpc/ratelimit@betabun add @orpc/ratelimit@betaBasic 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.
import { class ORPCError<TCode extends ORPCErrorCode, TData>Typed error carrying a `code`, a `message`, and optional `data`.
Throw it from handlers or middleware to produce typed error responses on the client.ORPCError } from '@orpc/server'
const const limiter: MemoryRateLimiterlimiter = new new MemoryRateLimiter(options: MemoryRateLimiterOptions): MemoryRateLimiterRate limiter adapter backed by in-memory storage. Enforces a fixed-window
limit, with optional blocking mode.MemoryRateLimiter({
MemoryRateLimiterOptions.maxRequests: numberMaximum number of requests allowed within the window.maxRequests: 5,
MemoryRateLimiterOptions.window: numberThe duration of the fixed window in milliseconds.window: 60000,
})
const const result: Required<RateLimitResult>result = await const limiter: MemoryRateLimiterlimiter.MemoryRateLimiter.limit(key: string, options?: RateLimitOptions): Promise<Required<RateLimitResult>>limit('user:123', { RateLimitOptions.weight?: number | undefinedThe weight of the request. Determines how many tokens or quota
units are consumed by this request. Must be an integer greater than 0.weight: 2 })
if (!const result: Required<RateLimitResult>result.success: booleanWhether the request may pass(true) or exceeded the limit(false)success) {
throw new new ORPCError<"TOO_MANY_REQUESTS", {
limit: number;
remaining: number;
reset: number;
}>(code: "TOO_MANY_REQUESTS", options: ORPCErrorOptions<{
limit: number;
remaining: number;
reset: number;
}>): ORPCError<"TOO_MANY_REQUESTS", {
limit: number;
remaining: number;
reset: number;
}>
Typed error carrying a `code`, a `message`, and optional `data`.
Throw it from handlers or middleware to produce typed error responses on the client.ORPCError('TOO_MANY_REQUESTS', {
data: {
limit: number;
remaining: number;
reset: number;
}
data: {
limit: numberlimit: const result: Required<RateLimitResult>result.limit: numberMaximum number of requests allowed within a window.limit,
remaining: numberremaining: const result: Required<RateLimitResult>result.remaining: numberHow many requests the user has left within the current window.remaining,
reset: numberreset: const result: Required<RateLimitResult>result.reset: numberUnix timestamp in milliseconds when the limits are reset.reset,
},
})
}
Adapters
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.
| Name | Blocking Mode | Adapter for |
|---|---|---|
MemoryRateLimiter |
✅ | In-memory storage |
RedisRateLimiter |
✅ | Redis |
UpstashRateLimiter |
✅ | Upstash Rate Limit |
BunRedisRateLimiter |
✅ | Bun’s Redis |
CloudflareRateLimiter |
❌ | Cloudflare’s Rate Limiting |
import { MemoryRateLimiter } from '@orpc/ratelimit/memory'
const limiter = new MemoryRateLimiter({
/**
* Maximum number of requests allowed within the window.
*/
maxRequests: 10,
/**
* The duration of the fixed window in milliseconds.
*/
window: 60000,
blockingUntilReady: {
/**
* Block until the request may pass or timeout is reached.
*
* @default false
*/
enabled: false,
/**
* milliseconds
*/
timeout: 5000
},
})import { RedisRateLimiter } from '@orpc/ratelimit/redis'
import { createClient } from 'redis'
const client = createClient({ url: 'redis://localhost:6379' })
// RedisRateLimiter lazily connects to Redis when needed.
// You can still call `client.connect()` manually, but it is optional.
await client.connect()
const limiter = new RedisRateLimiter(client, {
/**
* The prefix to use for Redis keys.
*
* @default ''
*/
prefix: '',
/**
* Maximum number of requests allowed within the window.
*/
maxRequests: 10,
/**
* The duration of the fixed window in milliseconds.
*/
window: 60000,
blockingUntilReady: {
/**
* Block until the request may pass or timeout is reached.
*
* @default false
*/
enabled: false,
/**
* milliseconds
*/
timeout: 5000
},
})import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
import { UpstashRateLimiter } from '@orpc/ratelimit/upstash'
const redis = Redis.fromEnv()
const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, '60 s'),
prefix: 'orpc:', // Optional key prefix
})
const limiter = new UpstashRateLimiter(ratelimit, {
blockingUntilReady: {
/**
* Block until the request may pass or timeout is reached.
*
* @default false
*/
enabled: false,
/**
* milliseconds
*/
timeout: 5000
},
/**
* For the MultiRegion setup we do some synchronizing in the background, after returning the current limit.
* Or when analytics is enabled, we send the analytics asynchronously after returning the limit.
* In most case you can simply ignore this.
*
* On Vercel Edge or Cloudflare workers, you might need `.bind` before assign:
* ```ts
* const ratelimiter = new UpstashRateLimiter(ratelimit, {
* waitUntil: ctx.waitUntil.bind(ctx),
* })
* ```
*/
waitUntil: undefined
})import { BunRedisRateLimiter } from '@orpc/bun'
import { redis } from 'bun'
const limiter = new BunRedisRateLimiter(redis, {
/**
* The prefix to use for Redis keys.
*
* @default ''
*/
prefix: '',
/**
* Maximum number of requests allowed within the window.
*/
maxRequests: 10,
/**
* The duration of the fixed window in milliseconds.
*/
window: 60000,
blockingUntilReady: {
/**
* Block until the request may pass or timeout is reached.
*
* @default false
*/
enabled: false,
/**
* milliseconds
*/
timeout: 5000
},
})import { CloudflareRateLimiter } from '@orpc/cloudflare'
export default {
async fetch(request, env) {
const limiter = new CloudflareRateLimiter(env.MY_RATE_LIMITER, {
/**
* The prefix to use for cloudflare ratelimit.
*
* @default ''
*/
prefix: ''
})
}
}Blocking Mode
Some adapters support blocking mode, which waits until capacity becomes available instead of rejecting requests immediately.
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.
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 } }
)
Handler Plugin
The RateLimitHandlerPlugin automatically adds HTTP rate limiting headers (RateLimit-* and Retry-After) to responses when used with Ratelimit Middleware. This lets clients inspect the current limit state and know when they can retry after hitting a limit.
import { RateLimitHandlerPlugin } from '@orpc/ratelimit'
const handler = new RPCHandler(router, {
plugins: [
new RateLimitHandlerPlugin(),
],
})