New in v0.6.0

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 modeWhen Redis is unavailable
open (default)Allow the request — don't take your traffic down over a cache outage.
closedBlock 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

5 failures →

OPEN

Skip Redis for 5s — use fail path

cooldown →

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:

ioredistypescript
new Redis(url, { commandTimeout: 200 });
redis-pypython
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:

MetricFires when
halt.request.fallbackA request was evaluated against the fallback store instead of Redis.
halt.redis.circuit_openThe 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.