rateLimit
Rate limits service method calls using rate-limiter-flexible. You provide a pre-configured RateLimiterAbstract instance (Memory, Redis, Mongo, etc.) and the hook consumes points per request.
import { rateLimit } from 'feathers-utils/hooks';Example
import { rateLimit } from 'feathers-utils/hooks'
import { RateLimiterMemory } from 'rate-limiter-flexible'
const rateLimiter = new RateLimiterMemory({ points: 10, duration: 1 })
app.service('users').hooks({
before: { find: [rateLimit(rateLimiter)] }
})Type declaration
Show Type Declarations
export type RateLimitOptions<H extends HookContext = HookContext> = {
/**
* The rate-limiting key, or a function to derive it from the context.
*
* Pass a static string to use a single shared bucket (a global rate limit
* across all requests), or a function to compute the key per request
* (e.g. per user or per IP).
*
* @default context.path
*/
key?: string | ((context: H) => Promisable<string>)
/**
* Number of points to consume per request, or a function to compute it from
* the context.
*
* @default 1
*/
points?: number | ((context: H) => Promisable<number>)
/**
* Lets a call over the limit through, to measure before enforcing: it still
* consumes its points and reaches `onLimited` and the headers, but gets no
* `Retry-After` and no `TooManyRequests`. A predicate decides per call, e.g.
* to enforce for some callers first.
*
* @default false
* @example
* ```ts
* rateLimit(rateLimiter, {
* dryRun: true,
* onLimited: (info, context) => logger.warn('over the limit', context.params.user?.id),
* })
* ```
*/
dryRun?: boolean | PredicateFn<H>
/**
* Called for every call over the limit, rejected or let through by `dryRun`,
* e.g. to log or count it. A store that fails is no call over the limit.
*/
onLimited?: (info: RateLimitInfo, context: H) => Promisable<void>
/**
* Tells the client where it stands, on `context.http.headers` for REST
* transports: `'legacy'` sends `X-RateLimit-Limit`, `-Remaining` and `-Reset`
* (epoch seconds), `'draft-6'`, `'draft-7'` and `'draft-8'` the `RateLimit`
* fields of the IETF draft in that version. A function returns headers of
* your own. A rejection adds `Retry-After` and carries the headers on
* `error.data.headers`, since Feathers' REST error handlers don't send
* hook headers.
*
* @default false
* @example
* ```ts
* rateLimit(rateLimiter, { headers: 'draft-8' })
* ```
*/
headers?:
| false
| RateLimitHeaderFormat
| ((info: RateLimitInfo, context: H) => Record<string, string>)
}
export type RateLimitHeaderFormat = "legacy" | "draft-6" | "draft-7" | "draft-8"
/** Where a caller stands, as the headers report it. */
export type RateLimitInfo = {
/** Points per window. */
limit: number
/** Points left in the current window. */
remaining: number
/** Seconds until the window resets. */
reset: number
/** The window, in seconds. */
window: number
/** Whether this call is over the limit: rejected, unless `dryRun`. */
limited: boolean
}
/**
* Rate limits service method calls using `rate-limiter-flexible`.
* You provide a pre-configured `RateLimiterAbstract` instance
* (Memory, Redis, Mongo, etc.) and the hook consumes points per request.
*
* @example
* ```ts
*
*
*
* const rateLimiter = new RateLimiterMemory({ points: 10, duration: 1 })
*
* app.service('users').hooks({
* before: { find: [rateLimit(rateLimiter)] }
* })
* ```
*
* @see https://utils.feathersjs.com/hooks/rate-limit.html
*/
export declare const rateLimit: <H extends HookContext = HookContext>(
rateLimiter: RateLimiterAbstract,
options?: RateLimitOptions<H>,
) => (context: H, next?: NextFunction) => Promise<void>| Argument | Type | Description |
|---|---|---|
| rateLimiter | RateLimiterAbstract | |
| options | RateLimitOptions<H> |
| type | methods | multi |
|---|---|---|
| before, around | find, get, create, update, patch, remove | yes |
The rateLimit hook limits how many times a service method can be called within a time window using rate-limiter-flexible. You provide a pre-configured rate limiter instance — the hook calls consume() on each request and throws a TooManyRequests error when the limit is exceeded. With dryRun, it lets such a call through and only reports it to onLimited, to measure a limit before enforcing it.
Any rate limiter backend supported by rate-limiter-flexible can be used (Memory, Redis, Mongo, Postgres, etc.).
Options
The RateLimiterRes is stored on context.params.rateLimit on both success and failure, so downstream hooks or services can inspect remainingPoints, consumedPoints, msBeforeNext, etc. It stays there: an error's data goes to the client, and on a shared bucket its points would tell every caller how much everyone else calls. A store that fails leaves its error there.
A rejection's TooManyRequests carries data.retryAfter, the seconds until the next call may pass, so a socket client can say when to try again.
Headers
With headers, a REST response tells the client where it stands. The formats follow express-rate-limit: 'legacy' for the widespread X-RateLimit-* headers, 'draft-6', 'draft-7' or 'draft-8' for that version of the IETF RateLimit header fields draft, or a function for a scheme of your own, such as X-RateLimit-Remaining-Requests.
Send them only where the bucket is the caller's own, keyed per user or IP. On a shared bucket (a static key, or the default context.path) remaining tells every caller how much everyone else is calling. On a login or another brute-force target, they help an attacker pace just under the limit; send no headers there and let data.retryAfter say when to try again.
A rejection adds Retry-After. Feathers' REST error handlers send only the error's status and body, not the headers hooks set, so the hook puts them on error.data.headers as well. Send them from a middleware before the error handler:
// Express
app.use((error, req, res, next) => {
if (error.data?.headers) res.set(error.data.headers)
next(error)
})
app.use(errorHandler())
// Koa, after `app.use(errorHandler())`
app.use(async (ctx, next) => {
try {
await next()
} catch (error) {
if (error.data?.headers) ctx.set(error.data.headers)
throw error
}
})Examples
Basic Usage
import { rateLimit } from 'feathers-utils/hooks'
import { RateLimiterMemory } from 'rate-limiter-flexible'
const rateLimiter = new RateLimiterMemory({
points: 10, // 10 requests
duration: 1, // per 1 second
})
app.service('users').hooks({
before: {
find: [rateLimit(rateLimiter)],
},
})Headers for REST Clients
app.service('messages').hooks({
before: {
create: [rateLimit(rateLimiter, { headers: 'draft-8' })],
},
})Rate Limit per User
Use the key option to rate limit per authenticated user instead of per service path:
const rateLimiter = new RateLimiterMemory({ points: 100, duration: 60 })
app.service('messages').hooks({
before: {
create: [
rateLimit(rateLimiter, {
key: (context) => `${context.path}:${context.params.user?.id}`,
}),
],
},
})Global Rate Limit
Pass a static string as the key to share a single bucket across all requests — a global cap on an endpoint instead of one bucket per context.path:
const rateLimiter = new RateLimiterMemory({ points: 1000, duration: 60 })
app.service('search').hooks({
before: {
find: [rateLimit(rateLimiter, { key: 'search' })],
},
})Custom Points per Request
Pass a static number to consume a fixed cost per request:
app.service('reports').hooks({
before: {
find: [rateLimit(rateLimiter, { points: 5 })],
},
})Or pass a function to compute the cost from the context — e.g. to charge more for expensive queries:
app.service('reports').hooks({
before: {
find: [
rateLimit(rateLimiter, {
points: (context) => (context.params.query?.$limit > 100 ? 5 : 1),
}),
],
},
})Redis Backend
import { RateLimiterRedis } from 'rate-limiter-flexible'
import Redis from 'ioredis'
const redisClient = new Redis()
const rateLimiter = new RateLimiterRedis({
storeClient: redisClient,
points: 100,
duration: 60,
keyPrefix: 'rl',
})
app.service('users').hooks({
before: {
find: [rateLimit(rateLimiter)],
},
})Bypass with iff
Use iff to skip rate limiting for internal (server-side) calls:
import { rateLimit, iff } from 'feathers-utils/hooks'
import { isProvider } from 'feathers-utils/predicates'
app.service('users').hooks({
before: {
find: [
iff(isProvider('rest', 'socketio', 'external'), rateLimit(rateLimiter)),
],
},
})Bypass with skippable
Use skippable to allow specific callers to opt out of rate limiting:
import { rateLimit, skippable } from 'feathers-utils/hooks'
app.service('users').hooks({
before: {
find: [skippable(rateLimit(rateLimiter))],
},
})
// Skip rate limiting for this call
app.service('users').find({ skipHooks: ['rateLimit'] })