@fastify/rate-limit counts per client in an onRequest hook, not at the route
A low overhead rate limiter for your routes
At a glance
- What is it?
- The @fastify/rate-limit plugin adds an onRequest hook to a Fastify server, tracks clients in an LRU cache keyed by IP, and answers over-limit callers with a 429. It suits a single process Fastify service that needs per-client limits without extra infrastructure, and it needs Redis the moment you run more than one instance.
- Who is it for?
- @fastify/rate-limit fits a Fastify service running on one instance that needs a sensible default limit, a 404 guard, and headers that let clients back off on their own, and it costs one npm install. It does not fit a deployment spread across several processes or servers, because the default store is in memory and the counters are per instance, so the documented answer there is an external Redis store through the redis option with ioredis.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The limit applies before your route handler runs
The plugin's mechanism is one hook and one counter store. Registering it adds an onRequest hook that checks whether a client, identified by IP address, has made too many requests inside the configured time window.
That placement is the design decision worth understanding. onRequest runs before routing and before your handler, so a rejected request costs the server almost nothing: no body parsing, no handler work, no database call. The documentation calls this a low overhead rate limiter, and the hook choice is where that claim comes from.
The hook is also configurable. The options list shows hook set to preHandler as the alternative to the onRequest default, which moves the check later in the lifecycle if you need request body or route context to be available, at the cost of doing more work before rejecting.
The plugin is a Fastify plugin, so it registers into an encapsulation scope. The global option, true by default, applies the limit to all routes in that scope, and false turns it off for the scope so you can opt routes in individually.
The user is any team running Fastify that has an endpoint worth protecting from a single client hammering it.
Registering the plugin with a limit and a window
Installation is a single npm command.
npm i @fastify/rate-limitRegistration follows the standard Fastify plugin pattern, and the two options you will set first are max and timeWindow.
import Fastify from 'fastify'
const fastify = Fastify()
await fastify.register(import('@fastify/rate-limit'), {
max: 100,
timeWindow: '1 minute'
})
fastify.get('/', (request, reply) => {
reply.send({ hello: 'world' })
})timeWindow accepts milliseconds, a string in the ms format, or an async function taking the request and the generated key that must return a number. The same is true of max, which can be an async function of request and key.
Making max a function is the interesting option. A single global number is usually wrong for a real API, because a login endpoint and a search endpoint do not deserve the same budget. Since the function receives the request, you can vary the limit per route or per client class.
The default values are worth knowing too: max defaults to 1000, timeWindow to one minute expressed as 1000 times 60, ban to -1, and cache to 5000 entries.
What a blocked client gets back
When a client exceeds the maximum, the plugin answers with a 429 and a body of this shape.
{
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded, retry in 1 minute'
}The response is not fixed. You can supply an errorResponseBuilder callback, or intercept it in a custom error handler, which the documentation links to the Fastify setErrorHandler reference.
fastify.setErrorHandler(function (error, request, reply) {
if (error.statusCode === 429) {
reply.code(429)
error.message = 'You hit the rate limit! Slow down please!'
}
reply.send(error)
})Alongside the status code, the plugin adds headers so a well behaved client can pace itself without parsing your error body. x-ratelimit-limit is how many requests the client can make, x-ratelimit-remaining is how many remain in the current window, x-ratelimit-reset is how many seconds must pass before the limit resets, and retry-after gives the seconds to wait once the maximum has been reached.
There is a second layer on top: ban. It sets the maximum number of 429 responses to return to a client before switching to 403 responses, and passing 0 makes the plugin return 403 directly once the client exceeds max. When the ban threshold is hit, the context passed to errorResponseBuilder has its ban property set to true, so you can shape a different body for a banned client.
Rate limiting the 404 path, which is where URL guessing happens
There is a security use case here that is easy to miss, and the documentation has a section on it.
If your 404 handler is not rate limited, an attacker can search for valid URLs by watching how quickly they can enumerate paths. The fix is to attach the rate limiter as a preHandler on the not found handler.
const fastify = Fastify()
await fastify.register(rateLimit, { global: true, max: 2, timeWindow: 1000 })
fastify.setNotFoundHandler({
preHandler: fastify.rateLimit()
}, function (request, reply) {
reply.code(404).send({ hello: 'world' })
})The same preHandler mechanism can be customised the way you would customise a route specific limit, and the documentation shows a variant with its own max and timeWindow, so the 404 path can carry its own budget independent of the rest of the API.
Note the syntax difference from route-level use. Registering the plugin with global: true and then calling fastify.rateLimit() with no arguments applies the plugin's own settings, while passing options to fastify.rateLimit() overrides them for that one handler.
The example directory also contains example-simple.mjs and example-auto-pipeline.mjs, and the latter name suggests a variant where the hook is attached automatically rather than by hand.
The key generator and why it matters behind a proxy
Clients are counted by key, and the default key generator is a function of the request that normalises the IP, with an ipv6Subnet option that defaults to 64.
keyGenerator: function (request) { /* ... */ }, // default (request) => normalizeIP(request.ip, ipv6Subnet)
ipv6Subnet: 64, // default 64Two things follow. First, IPv6 is handled by subnet rather than by exact address, so a single client with a large prefix allocation is counted as one, which is the right behaviour for rate limiting but changes what the counter means. Second, the default keys on request.ip, and if your server sits behind a proxy that address is the proxy's address unless you have configured Fastify's trustProxy setting. Behind a misconfigured proxy every client can share one bucket, which is the classic way a rate limiter silently stops working.
The keyGenerator option exists for exactly that reason, and for the case where you want to key on an authenticated user id or an API key rather than an address. The allowList option complements it, excluding addresses from limiting entirely, and it can itself be a function of request and key, so you can exempt a service account or a health checker without hardcoding it.
The continueExceeding option, default false, changes what happens to a client that keeps going over the limit.
One process or many: the store is the decision
The cache option sets the size of an internal LRU cache, 5000 entries by default, and the plugin uses it to track clients in memory.
That is the right default for a single Fastify process, where an in-memory counter is faster than a network round trip and disappears with the process. It is the wrong default the moment you scale out. The documentation says so plainly: if an application runs on multiple servers, an external store will be needed, and the plugin requires the use of ioredis for that.
redis: new Redis({ host: '127.0.0.1' }), // default null
nameSpace: 'teste-ratelimit-', // default is 'fastify-rate-limit-'The namespace option exists so several applications sharing one Redis instance do not collide on their keys.
The redis option also carries a warning that the default settings of an ioredis instance are not optimal for rate limiting, and recommends customising connectTimeout. The repository has a store directory, which is where the alternative store implementations live, and the package.json has a redis script that starts a local Redis in Docker for development.
"redis": "docker run -p 6379:6379 --name rate-limit-redis -d --rm redis",If you cannot add a shared store, the honest alternative is to rate limit at the edge, in a reverse proxy, where the counter is global rather than per instance. The plugin cannot make an in-memory counter consistent across processes.
Version pairing, headers you can trim, and the MIT licence
Compatibility is tabulated because the plugin follows Fastify's own support lifecycle.
| Plugin version | Fastify version | | -------------- | -------------------- | | `>=10.x` | `^5.x` | | `>=7.x <10.x` | `^4.x` | | `>=3.x <7.x` | `^3.x` | | `>=2.x <7.x` | `^2.x` | | `^1.x` | `^1.x` |
The note is that if a Fastify version is out of support, the corresponding versions of this plugin are out of support too, with a link to Fastify's LTS policy. The current release is 11.2.1, published 2026-10-08, with 11.2.0 in July and 11.1.0 in June, and the last push was on 2026-10-08.
Two header options let you control what clients see. addHeadersOnExceeding applies to responses when the limit has not been reached, and addHeaders applies when it has, with the retry-after header included in the second set. Both default to showing all four headers, and both take per header booleans, so you can hide the ones you do not want to publish.
There is also an enableDraftSpec option, default false, which switches to the draft header standard. Its comment in the options list contains a typo for the standards body name, which is a small sign of how lightly that line is maintained.
The licence is MIT, the package is CommonJS with TypeScript types at types/index.d.ts, and the test suite runs with node --test under c8 with 100 percent coverage enforced, plus a separate tstyche pass for the type tests.
Editorial conclusion
@fastify/rate-limit fits a Fastify service running on one instance that needs a sensible default limit, a 404 guard, and headers that let clients back off on their own, and it costs one npm install. It does not fit a deployment spread across several processes or servers, because the default store is in memory and the counters are per instance, so the documented answer there is an external Redis store through the redis option with ioredis. Before adopting it, confirm the Fastify major version against the compatibility table so the plugin version you pick matches the framework version in support, check whether you need the ban behaviour and the draft spec headers, and remember that the key generator defaults to normalising the client IP, which matters directly behind a proxy.
Frequently asked questions
How do I add rate limiting to a Fastify server?
Register the plugin with fastify.register(import('@fastify/rate-limit'), { max, timeWindow }). It adds an onRequest hook that counts requests per client IP and answers over-limit callers with a 429 status code.
Does @fastify/rate-limit work across multiple servers?
Not with the default store, which is an in-memory LRU cache. The documentation states that an application running on multiple servers needs an external store, and the plugin requires ioredis for that.
How can I rate limit 404 responses in Fastify?
Attach the limiter as a preHandler on the not found handler with fastify.setNotFoundHandler({ preHandler: fastify.rateLimit() }, handler), optionally passing max and timeWindow to that call for a stricter limit than the rest of the API.
Which Fastify version does @fastify/rate-limit v11 need?
The compatibility table lists plugin versions of 10.x and above for Fastify 5.x, 7.x to 10.x for Fastify 4.x, and 3.x to 7.x for Fastify 3.x. The note is that if a Fastify version is out of support, so are the matching plugin versions.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/fastify-fastify-rate-limit)Was this useful?