Recommended for production

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 RedisStore and AsyncRedisStore.
  • Bring your own client. Halt doesn't bundle a Redis driver you inject a connected client.

Installation

Pythonbash
pip install halt-rate[redis]
TypeScriptbash
npm install ioredis

TypeScript 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:

OptionDefaultMeaning
client (required)A connected Redis client you inject (ioredis / node-redis, or redis / redis.asyncio).
failMode / fail_modeopenWhat happens when Redis is unavailable: open allows, closed blocks (429), fallback evaluates locally. See Resilience.
fallbackStore / fallback_storeLocalStore()Store used when failMode is fallback. Inject a custom AtomicStore if you like.
circuitBreaker / circuit_breakeron (5 / 5000ms)Stops hammering a down Redis. Pass false to disable or custom thresholds. See Resilience.
onError / on_errorCalled with the underlying error on any Redis failure.
metricsRecorder / metrics_recorderMetrics 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)