Resilience
Your rate limiter shouldn't take your API down when Redis has a bad day. Halt degrades gracefully — keep enforcing limits locally, and stop a slow or dead Redis from cascading.
Fallback mode
On a Redis error, evaluate against a local store — still enforce limits instead of a blanket allow.
Circuit breaker
On by default. Stops piling requests onto a down Redis and probes for recovery.
Timeout-aware
Set the client command timeout so a latency spike trips the breaker instead of hanging.
Fail modes
The Redis store takes a failMode (TypeScript) / fail_mode (Python) that decides what happens when Redis is unreachable or erroring:
| Fail mode | When Redis is unavailable |
|---|---|
open (default) | Allow the request — don't take your traffic down over a cache outage. |
closed | Block the request (429). Best for abuse-sensitive routes. |
fallback (new) | Evaluate against a local LocalStore — still enforces, but per-instance and approximate. |
Fallback is per-instance
Each process keeps its own local counters while Redis is down, so with N instances the global limit can drift up to N×limit. That's the trade — approximate limiting during an outage beats none. Counters reconverge on Redis's own state once it recovers.
TypeScript
import Redis from 'ioredis';
import { RateLimiter, RedisStore, LocalStore, presets } from 'halt-rate';
const store = new RedisStore({
// commandTimeout makes a slow Redis surface as an error (see below).
client: new Redis(process.env.REDIS_URL, { commandTimeout: 200 }),
failMode: 'fallback', // 'open' | 'closed' | 'fallback'
fallbackStore: new LocalStore(), // optional — this is the default
circuitBreaker: { failureThreshold: 5, cooldownMs: 5000 }, // default; or false to disable
onError: (err) => console.error('halt.redis', err),
});
const limiter = new RateLimiter({ store, policy: presets.PUBLIC_API });Python
import redis
from halt import RateLimiter, RedisStore, LocalStore, presets
store = RedisStore(
client=redis.Redis.from_url(
"redis://localhost:6379",
socket_timeout=0.2, # surface a slow Redis as an error
),
fail_mode="fallback", # "open" | "closed" | "fallback"
fallback_store=LocalStore(), # optional — this is the default
circuit_breaker=None, # default on; pass False to disable
)
limiter = RateLimiter(store=store, policy=presets.PUBLIC_API)The same options apply to AsyncRedisStore.
Circuit breaker
A dead or slow Redis is dangerous not just because checks fail, but because every request keeps trying — piling latency and connections onto a struggling server. Halt's circuit breaker cuts that off. It's enabled by default.
CLOSED
Normal — calls hit Redis
OPEN
Skip Redis for 5s — use fail path
HALF-OPEN
One probe → CLOSED on success
While the breaker is open, requests are served straight from the fail path (fallback / open / closed) without touching Redis — so a down Redis can never cascade into your request latency. After the cooldown, a single probe decides whether to close again.
Tune or disable it:
// Custom thresholds
new RedisStore({ client, circuitBreaker: { failureThreshold: 10, cooldownMs: 2000 } });
// Disable entirely
new RedisStore({ client, circuitBreaker: false });CircuitBreaker is also exported on its own if you want to wrap other fallible calls with the same logic.
Latency spikes
Halt deliberately does not add its own per-call timeout — that would mean guessing your infrastructure's latency budget. Instead, configure the client's command timeout so a hung Redis surfaces as an error that trips the fallback and the breaker:
new Redis(url, { commandTimeout: 200 });redis.Redis.from_url(url, socket_timeout=0.2)LocalStore
LocalStore is a standalone atomic in-memory store (evaluate / aevaluate) that runs the same four algorithms as the distributed path. It's the default fallback, and it's also a good primary store for single-process apps — the same role InMemoryStore plays, exported for direct use and injection.
Metrics
Resilience events flow through the same telemetry hook as everything else. Two new counters land when degradation happens:
| Metric | Fires when |
|---|---|
halt.request.fallback | A request was evaluated against the fallback store instead of Redis. |
halt.redis.circuit_open | The circuit breaker opened (Redis is being skipped). |
Alert on a rising halt.redis.circuit_open — it's your earliest signal that Redis is unhealthy, well before users notice.
Recommended production setup
- Set a client
commandTimeout/socket_timeout(100–250 ms is typical). - Use
failMode: 'fallback'for general APIs,'closed'for abuse-sensitive routes. - Leave the circuit breaker on (the default).
- Wire a telemetry hook and alert on
halt.redis.circuit_open.