Redis Storage
Atomic, distributed, cluster-safe rate limiting for multi-instance deployments.
The RedisStore is the recommended backend for any service that runs more than one process. Every limit decision is computed inside Redis by a single-key Lua script, so a burst of concurrent requests can never over-admit past the configured limit even across a fleet of app servers.
- Atomic. All four algorithms run as single-key Lua scripts in Redis.
- Cluster-safe. Each script touches exactly one key, so Redis Cluster works out of the box.
- Fail-open by default. A Redis outage allows traffic instead of taking your API down (configurable).
- Sync & async. Python ships both
RedisStoreandAsyncRedisStore. - Bring your own client. Halt doesn't bundle a Redis driver you inject a connected client.
Installation
pip install halt-rate[redis]npm install ioredisTypeScript works with ioredis or node-redis v4+. Python wraps the official redis package (redis.Redis and redis.asyncio.Redis).
Usage
TypeScript
import Redis from 'ioredis';
import { RateLimiter, RedisStore, presets } from 'halt-rate';
const store = new RedisStore({
client: new Redis(process.env.REDIS_URL),
failMode: 'open', // 'open' (default) allows on a Redis outage; 'closed' blocks (429)
onError: (err) => console.error('redis rate-limit error', err),
});
const limiter = new RateLimiter({ store, policy: presets.PUBLIC_API });
const decision = await limiter.check(req);Pass the same store to the Express or Next.js adapters nothing else changes.
Python synchronous
import redis
from halt import RateLimiter, RedisStore, presets
store = RedisStore(
client=redis.Redis.from_url("redis://localhost:6379"),
fail_mode="open",
)
limiter = RateLimiter(store=store, policy=presets.PUBLIC_API)
decision = limiter.check(request)Python async (FastAPI / asyncio)
Use AsyncRedisStore with await limiter.acheck(...). The FastAPI middleware already calls acheck internally, so an async store works end-to-end.
import redis.asyncio as aioredis
from halt import RateLimiter, AsyncRedisStore, presets
store = AsyncRedisStore(client=aioredis.Redis.from_url("redis://localhost:6379"))
limiter = RateLimiter(store=store, policy=presets.PUBLIC_API)
decision = await limiter.acheck(request)Configuration
Both the TypeScript and Python stores accept the same options:
| Option | Default | Meaning |
|---|---|---|
client | (required) | A connected Redis client you inject (ioredis / node-redis, or redis / redis.asyncio). |
failMode / fail_mode | open | What happens when Redis is unavailable: open allows, closed blocks (429), fallback evaluates locally. See Resilience. |
fallbackStore / fallback_store | LocalStore() | Store used when failMode is fallback. Inject a custom AtomicStore if you like. |
circuitBreaker / circuit_breaker | on (5 / 5000ms) | Stops hammering a down Redis. Pass false to disable or custom thresholds. See Resilience. |
onError / on_error | Called with the underlying error on any Redis failure. | |
metricsRecorder / metrics_recorder | Metrics hook (see below). |
Resilience — what happens when Redis is down
The default is fail-open: if Redis is unreachable, requests are allowed so a cache outage doesn't take your traffic down. You can also fail closed (429 for abuse-sensitive routes) or, new in v0.6.0, fallback to a local store so limits keep being enforced. A default-on circuit breaker stops a dead Redis from cascading.
Under the hood
- Token-bucket, fixed-window, and leaky-bucket use a Redis HASH per key.
- Sliding-window uses a Redis sorted set as a true request log (
ZADD/ZREMRANGEBYSCORE/ZCARD). - Scripts read time from the Redis server (
TIME), so app-server clock skew never affects accuracy.
Redis Cluster
Every script touches exactly one key, so Redis Cluster is supported with no extra work. To colocate related keys on the same hash slot, wrap the variable part of the key in a hash tag, e.g. halt:{user-123}:....
Metrics
When you pass a metrics hook, Halt emits these counters:
halt.request.checked
halt.redis.error
halt.request.fail_open
halt.request.fail_closed
halt.request.fallback # evaluated against the fallback store (v0.6.0)
halt.redis.circuit_open # circuit breaker opened (v0.6.0)