Silgi
Plugins

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. Silgi 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 'silgi/ratelimit'

const  = ({
  : new ({
    : 100, // max 100 requests
    : 60_000, // per 60 seconds
  }),
  : () => . ?? 'anonymous',
})

Then add it to any procedure:

const  = s.$use(rateLimit).$resolve(({  }) => .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  = s.$use(rateLimit).$resolve(({  }) => {
  // ctx.rateLimit is available after the guard runs
  .(.rateLimit.remaining) // 97
  return .db.users.findMany()
})

The ctx.rateLimit object has:

PropertyTypeDescription
successbooleanAlways true (the guard throws if false)
limitnumberMaximum requests allowed
remainingnumberRequests remaining in the current window
resetnumberUnix timestamp (ms) when the window resets

MemoryRateLimiter

The built-in limiter stores counters in memory using a sliding window algorithm:

import {  } from 'silgi/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 "silgi/ratelimit"

interface RateLimiter {
(: string): <RateLimitResult>
}

interface RateLimitResult {
: boolean
: number
: number
: number // Unix timestamp in ms
}
import type { RateLimiter, RateLimitResult } from "silgi/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

OptionTypeRequiredDescription
limiterRateLimiterYesThe rate limiter instance (in-memory, Redis, etc.)
keyFn(ctx) => string | Promise<string>YesExtract the rate limit key from the context
messagestringNoCustom 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  = s.$use().$resolve(...)
const  = s.$use(auth, ).$resolve(...)

What's next?

  • Middleware — understand guards, which power the rate limiter
  • Typed Errors — how the TOO_MANY_REQUESTS error works on the client
  • Plugins — other available plugins

On this page