Rate limiting for Elysia routes, guards, and groups, as a macro.
Drop rateLimit on any endpoint and abusive traffic gets cut before it ever hits your logic.
This plugin uses Elysia's macro system to add rate limiting to any route, guard, or group.
You add rateLimit: { limit: 10, window: 60 } and you're done.
By default it limits by client IP, which works great for auth endpoints.
For cases where many users share the same public IP (offices, corporate proxies), you can pass a keyGenerator to rate limit by IP + access token, session ID, API key, or any combination that makes sense.
Storage is handled by @clov-std/kv-store, so you start with in-memory and move to Redis when you need to, without changing your routes.
rateLimit to any route independently, with its own limit and window.MemoryStore out of the box; swap in BunRedisStore or your own adapter.transform, the first per-route hook, before auth guards and handlers.X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset.429 : Answers a real problem document on its own, detail included, with no exception class to import.bun add @clov-std/elysia-ratelimit elysia typebox
Peer dependencies:
elysia(v2) andtypeboxmust be installed alongside.
The simplest form: pass limit (max requests) and window (time in seconds). Each client IP gets its own counter.
import { rateLimitPlugin } from '@clov-std/elysia-ratelimit';
import { Elysia } from 'elysia';
new Elysia()
.use(rateLimitPlugin())
.post(
'/auth/login',
{ rateLimit: { limit: 10, window: 60 } }, // 10 requests per minute per IP
() => authenticate()
)
.listen(3000);
By default, counters are kept in memory. Pass a BunRedisStore (or any KvStore adapter) for persistence across restarts and multi-instance deployments.
import { BunRedisStore } from '@clov-std/kv-store';
import { rateLimitPlugin } from '@clov-std/elysia-ratelimit';
import { Elysia } from 'elysia';
const store = new BunRedisStore('redis://localhost:6379');
new Elysia()
.use(rateLimitPlugin(store))
.post('/auth/login', { rateLimit: { limit: 10, window: 60 } }, () => authenticate())
.listen(3000);
Useful for authenticated routes where many users share the same public IP (office, corporate proxy).
Each user has their own counter, independent of their network.
import { rateLimitPlugin } from '@clov-std/elysia-ratelimit';
import { Elysia } from 'elysia';
new Elysia()
.use(rateLimitPlugin())
.get(
'/api/data',
{
rateLimit: {
limit: 100,
window: 60,
keyGenerator: ({ ip, request }) =>
`${ip}:${request.headers.get('authorization') ?? ip}`
}
},
() => getData()
)
.listen(3000);
Tip:
extractClientIpis exported if you need it inside your ownkeyGenerator.
When the allowance is exceeded, the plugin throws Elysia's problem(), so the 429 is a complete
RFC 9457 document with no exception class and no error dependency involved:
{
"type": "rate-limit.quota.exceeded",
"title": "Too Many Requests",
"status": 429,
"detail": "Limit of 100 requests per 60s exceeded. Retry in 42s."
}
type is always RATE_LIMIT_ERROR_CODES.QUOTA_EXCEEDED. title is derived by Elysia from the
status reason phrase. detail carries the live counter state, so it changes per occurrence.
The content type is application/problem+json, and the X-RateLimit-* headers are set before
the throw so they survive on the 429.
This is also the response the macro declares for OpenAPI, so a typed client sees it.
Full docs: https://clovlabs.github.io/std/
MIT — see LICENSE.md.
Maintained by Clov.