--- title: Rate limiting description: Redis-backed sliding-window rate limiting. type: reference related: - /en/docs/packages/security/application - /en/docs/packages/authentication --- # Rate limiting ```ts import { createRateLimiter, slidingWindow } from '@kreogen/rate-limit'; const limiter = createRateLimiter({ limiter: slidingWindow(10, '10 s'), prefix: 'contact-form', }); const { success, remaining, reset } = await limiter.limit(identifier); if (!success) { throw new Error('Too many requests. Try again shortly.'); } ``` Windows accept `ms`, `s`, `m`, `h` and `d` — `'30 s'`, `'1h'`, `'1d'`. ## How it works Each identifier gets a Redis sorted set of request timestamps. On every call a single Lua script trims entries older than the window, counts what remains, and inserts the new one only if the count is under the limit. Doing all three in one script matters: split into separate commands, two concurrent requests can both read a count below the limit and both be allowed. ## It fails open When `REDIS_URL` is unset — or Redis is unreachable — requests are allowed. That is deliberate. A limiter exists to protect a feature, and taking the feature down when the limiter itself is unavailable inverts the tradeoff. If a particular route needs the opposite behaviour, check `redis` directly and decide for yourself: ```ts import { redis } from '@kreogen/rate-limit'; if (!redis) { throw new Error('Rate limiting is required for this endpoint.'); } ``` ## Configuration | Variable | Required | Purpose | | ----------- | -------- | ----------------------------- | | `REDIS_URL` | no | `redis://` or `rediss://` URL | Redis ships in `docker-compose.yml`, so locally this needs no setup beyond `bun run docker:up`. In production point it at whatever Redis the project uses — managed or self-hosted, both work, because this uses the standard protocol rather than a vendor REST API. ## Choosing an identifier Prefer the authenticated user. Fall back to `clientIdentifier`, never to the raw header: ```ts import { clientIdentifier } from '@kreogen/rate-limit'; import { auth } from '@kreogen/auth/session'; import { headers } from 'next/headers'; const { userId } = await auth(); const identifier = userId ?? clientIdentifier(await headers()); ``` `headers().get('x-forwarded-for')` is not an identifier. XFF is a list each proxy appends to, so the **leftmost** entry is whatever the original client sent — attacker-controlled and free-form. A limiter keyed on it is not a limiter: the caller sends a fresh random value on every request, lands in a fresh bucket every time, and never hits the limit. `clientIdentifier` counts from the right instead, since only the entries appended by infrastructure you control are trustworthy. It defaults to one proxy, which is the shape of every single-load-balancer deployment: ```ts clientIdentifier(requestHeaders, { trustedProxyCount: 2 }); ``` Match that number to the deployment. Too high and the index walks back into the attacker-controlled prefix, reintroducing exactly the spoof it exists to prevent. Too low and every caller behind the outermost proxy shares one bucket. Even correct, IP is a blunt instrument: users behind shared egress share a bucket. Use it for unauthenticated surfaces and the user id everywhere else. ## Redis connection behaviour Two settings are load-bearing for the fail-open promise, and both are the opposite of ioredis' defaults: * **`enableOfflineQueue: false`.** On, commands issued while the socket is down are buffered and replayed on reconnect — so a request that should have been answered in milliseconds hangs for as long as the outage lasts. Off, it rejects immediately and `limit()` falls through to allowing the request. * **`lazyConnect: true`.** Importing the package opens no socket, so a build step or a test never connects. An `error` listener is attached for the same reason. ioredis emits `error` on every failed connection attempt, and Node throws on an unhandled one — so a Redis that is merely slow to start would take the whole process down, which is the precise opposite of failing open. --- For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md) For an index of all available documentation, see [/llms.txt](/llms.txt)