Rate Limiting
Sliding window rate limiter — protect your API with in-memory or pluggable backends like Redis.
Rate limiting prevents clients from making too many requests in a short period. Katman provides a guard middleware that uses a sliding window algorithm and works with an in-memory store or any custom backend.
Quick start
import { , } from "katman/ratelimit"
const = ({
: new ({
: 100, // max 100 requests
: 60_000, // per 60 seconds
}),
: () => . ?? "anonymous",
})Then add it to any procedure:
const = k.query({
: [rateLimit],
: ({ }) => .db.users.findMany(),
})When a client exceeds the limit, the guard throws a TOO_MANY_REQUESTS error:
{
"code": "TOO_MANY_REQUESTS",
"status": 429,
"message": "Rate limit exceeded",
"data": {
"limit": 100,
"remaining": 0,
"reset": 1710720000000,
"retryAfter": 42
}
}The retryAfter field tells the client how many seconds to wait before trying again.
How the key function works
The keyFn determines who is being rate-limited. Different keys mean different rate limit buckets:
// Rate limit by IP address
keyFn: () => .ip ?? "anonymous"
// Rate limit by authenticated user
keyFn: () => .user?.id ?? .ip
// Rate limit by API key
keyFn: () => .headers["x-api-key"] ?? "no-key"Each unique key gets its own counter. So if you key by user ID, one user hitting the limit doesn't affect other users.
Context enrichment
When the guard passes (the client is within the limit), it adds rate limit info to the context:
const = k.query({
: [rateLimit],
: ({ }) => {
// ctx.rateLimit is available after the guard runs
.(.rateLimit.remaining) // 97
return .db.users.findMany()
},
})The ctx.rateLimit object has:
| Property | Type | Description |
|---|---|---|
success | boolean | Always true (the guard throws if false) |
limit | number | Maximum requests allowed |
remaining | number | Requests remaining in the current window |
reset | number | Unix timestamp (ms) when the window resets |
MemoryRateLimiter
The built-in limiter stores counters in memory using a sliding window algorithm:
import { } from "katman/ratelimit"
const = new ({
: 100, // max requests per window
: 60_000, // window duration in milliseconds
})This works well for single-server deployments. Counters are lost when the server restarts, which is usually fine — rate limiting is about preventing abuse, not exact accounting.
For multi-server deployments, use a shared backend like Redis. See below for how to plug in a custom rate limiter.
Custom backend
The rate limiter is an interface with a single method. Implement it for Redis, Upstash, DynamoDB, or any other backend:
import type { RateLimiter, RateLimitResult } from "katman/ratelimit"
interface RateLimiter {
(: string): <RateLimitResult>
}
interface RateLimitResult {
: boolean
: number
: number
: number // Unix timestamp in ms
}import type { RateLimiter, RateLimitResult } from "katman/ratelimit"
import from "ioredis"
class implements RateLimiter {
#redis = new ()
#limit: number
#windowMs: number
constructor(: number, : number) {
this.#limit =
this.#windowMs =
}
async (: string): <RateLimitResult> {
const = .()
const = `rl:${}:${.( / this.#windowMs)}`
const = await this.#redis.incr()
if ( === 1) await this.#redis.pexpire(, this.#windowMs)
return {
: <= this.#limit,
: this.#limit,
: .(0, this.#limit - ),
: (.( / this.#windowMs) + 1) * this.#windowMs,
}
}
}
// Use it like the MemoryRateLimiter
const = rateLimitGuard({
: new (100, 60_000),
: () => .ip,
})Options
| Option | Type | Required | Description |
|---|---|---|---|
limiter | RateLimiter | Yes | The rate limiter instance (in-memory, Redis, etc.) |
keyFn | (ctx) => string | Promise<string> | Yes | Extract the rate limit key from the context |
message | string | No | Custom error message (default: "Rate limit exceeded") |
Different limits per endpoint
Create separate rate limit guards for different limits:
const = rateLimitGuard({
: new MemoryRateLimiter({ : 100, : 60_000 }),
: () => .ip,
})
const = rateLimitGuard({
: new MemoryRateLimiter({ : 1000, : 60_000 }),
: () => .user.id,
})
const = k.query({ : [], : ... })
const = k.mutation({ : [auth, ], : ... })What's next?
- Middleware — understand guards, which power the rate limiter
- Typed Errors — how the
TOO_MANY_REQUESTSerror works on the client - Plugins — other available plugins