The @fastify/rate-limit plugin uses an in memory store by default. Each instance keeps its own counts, so with four servers behind a balancer, a client can make up to four times your intended limit. Use an external store such as Redis, or a custom store, when you run more than one instance.
What the README says
The plugin documentation states that by default it uses an in memory store, but if an application runs on multiple servers an external store will be needed. It supports Redis through the ioredis client and also a custom store option for an RDBMS, MongoDB or your own logic.
It also notes that the default settings of an ioredis instance are not optimal for rate limiting, and recommends customizing connectTimeout and maxRetriesPerRequest as in its example.
The failure modes
| Setup | Effect |
|---|---|
| One instance, memory store | Works as written |
| Many instances, memory store | Effective limit is multiplied, and counts reset on restart or deploy |
| Many instances, shared store | One consistent count |
| Shared store unreachable | Depends on skipOnError |
The skipOnError option decides what happens when the store errors, for example Redis is down. It defaults to false. If you set it to true, errors from storage are skipped, which keeps the app up but removes your limit during an outage. Choose deliberately. For a login route you may prefer to fail closed.
Other options worth knowing
| Option | Purpose |
|---|---|
| nameSpace | Prefix for keys in Redis, default fastify-rate-limit- |
| allowList | IPs or a function that skips limiting |
| keyGenerator | What identifies a client, default is normalized request.ip |
| groupId | Separate limits for groups of routes |
Be careful with allowList. Every exemption is a hole, and addresses that look internal behind a proxy may be someone else if forwarded headers are trusted too widely. See Fastify trustProxy and rate limit keys.
Steps
- Count your running instances, including autoscaled ones.
- If more than one, set up a shared store and pass it to the plugin.
- Set connection timeouts so a slow store does not stall requests.
- Decide skipOnError per route class.
- Use a unique nameSpace per environment so staging and production do not share counters.
- Test by sending requests through the balancer and counting where the limit triggers.
await fastify.register(require('@fastify/rate-limit'), {
max: 100,
timeWindow: '1 minute',
redis: redisClient,
nameSpace: 'prod-api-'
});
Beyond counting
Limits are one control among several. A limiter that counts wrongly gives false comfort, see four silent ways a rate limiter permits everything, and what is rate limiting for the basics.
Safeguard's asset discovery can help you find all running services that should be covered, and a free dependency scan is at gold.safeguard.sh/scan.
Quick check
Is the in memory store wrong for production?
Not for a single instance. With several instances, each keeps separate counts, so use a shared store.
What happens to limits when Redis goes down?
It depends on skipOnError. The default is false. If true, storage errors are skipped and requests go through unlimited.
Sources
Last reviewed: 19 October 2026.