zeroae / zeroae/zae-limiter

✨ Add Redis backend support (optional)

Open
#149 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

api-design area/limiter performance
Dominant language
Python
Stars
0
Forks
0
Avg merge
6h 51m
Merged PRs (30d)
104

Description

Summary

Add an optional Redis backend as an alternative to DynamoDB for users who need sub-millisecond latency or already have Redis infrastructure.

Motivation

Aspect DynamoDB Redis
Latency (p50) 36-51ms <1ms
Distributed ✅ Built-in ✅ Cluster mode
AWS-native ✅ Yes ⚠️ ElastiCache or self-hosted
Serverless ✅ On-demand billing ❌ Always-on instances
Cost model Pay per request Pay per hour

Redis wins when:

  • Sub-millisecond latency is critical
  • High-frequency rate limiting (10k+ RPS)
  • Already running Redis in your stack
  • Running on EKS with existing Redis infrastructure

DynamoDB wins when:

  • LLM workloads (latency doesn't matter, 36-51ms << 100ms+ LLM calls)
  • Serverless / pay-per-use cost model
  • No Redis infrastructure to manage
  • Audit logging / compliance needs

Non-Breaking Implementation

1. Define Repository Protocol
# repository_protocol.py (new file)
from typing import Protocol

class RepositoryProtocol(Protocol):
    async def get_entity(self, entity_id: str) -> Entity | None: ...
    async def create_entity(self, entity_id: str, ...) -> Entity: ...
    async def get_buckets(self, entity_id: str, resource: str) -> list[BucketState]: ...
    async def transact_consume(self, entries: list[ConsumeEntry]) -> None: ...
    async def close(self) -> None: ...
    # ... other methods needed by RateLimiter
2. Accept optional repository parameter in RateLimiter
class RateLimiter:
    def __init__(
        self,
        name: str = "limiter",
        region: str | None = None,
        endpoint_url: str | None = None,
        stack_options: StackOptions | None = None,
        # NEW - optional, defaults to DynamoDB
        repository: RepositoryProtocol | None = None,
        ...
    ):
        if repository is not None:
            self._repo = repository
        else:
            # Current behavior - create DynamoDB repository
            self._repo = Repository(...)
3. Usage (backwards compatible)
# Existing code - unchanged, still works
limiter = RateLimiter(
    name="my-app",
    region="us-east-1",
)

# New option for Redis users
from zae_limiter.backends.redis import RedisRepository

limiter = RateLimiter(
    name="my-app",
    repository=RedisRepository(url="redis://localhost:6379"),
)

Package Structure

Recommended: Optional dependency in main package

# pyproject.toml
[project.optional-dependencies]
redis = ["redis>=5.0"]
# Users install
pip install zae-limiter[redis]
from zae_limiter.backends.redis import RedisRepository

Benefits:

  • Single package, simpler for users
  • Shared test infrastructure
  • Easier to maintain feature parity
  • No breaking changes

Redis Implementation Details

Key Structure
ratelimit:{name}:entity:{entity_id}:meta           # JSON: {parent_id, name, cascade, created_at}
ratelimit:{name}:entity:{entity_id}:{resource}:{limit_name}  # JSON: {tokens_milli, last_update_ms, version}
ratelimit:{name}:children:{parent_id}              # SET: child entity IDs
Cascade Support (Lua Script)
-- acquire.lua
local entity_key = KEYS[1]
local bucket_keys = {KEYS[2], KEYS[3], ...}

-- 1. Get entity meta
local meta = redis.call('GET', entity_key)
local parent_id = cjson.decode(meta).parent_id

-- 2. If cascade, get parent buckets too
if parent_id and cascade then
    -- Add parent bucket keys to check
end

-- 3. Check all limits (token bucket math)
-- 4. MULTI/EXEC to update all buckets atomically
Feature Parity
Feature DynamoDB Redis Notes
Token bucket Lua script
Hierarchical limits Key patterns + Lua
Cascade Lua script handles parent lookup
Audit logging ⚠️ Redis Streams (different API)
Usage snapshots ⚠️ Would need separate consumer
TTL cleanup Native EXPIRE
Transactions ⚠️ Lua scripts are atomic
What's NOT included initially
  • Audit logging (could use Redis Streams, but different pattern)
  • Usage snapshots (requires stream consumer, not Lambda)
  • Infrastructure management (no CloudFormation equivalent)

Acceptance Criteria

Phase 1: Protocol Extraction
  • Define RepositoryProtocol with all methods used by RateLimiter
  • Existing Repository implements the protocol (implicit, via duck typing)
  • Add optional repository parameter to RateLimiter.__init__
  • No changes to existing behavior when repository=None
  • Update SyncRateLimiter similarly
Phase 2: Redis Backend
  • RedisRepository implements RepositoryProtocol
  • Token bucket via Lua scripts
  • Entity CRUD operations
  • Cascade support via Lua
  • Connection pooling
  • Proper async support (redis-py async)
Phase 3: Testing
  • Unit tests with mocked Redis
  • Integration tests with real Redis (Docker)
  • Benchmark comparisons vs DynamoDB
  • Feature parity tests (same tests run against both backends)
Phase 4: Documentation
  • Backend selection guide in docs
  • Redis-specific configuration options
  • Performance comparison with real numbers
  • Migration guide (DynamoDB → Redis, if applicable)

Effort Estimate

Phase Effort
Protocol extraction 1-2 days
Redis backend (basic) 1 week
Cascade support 2-3 days
Testing 1 week
Documentation 2-3 days
Total 2-4 weeks

Questions to Resolve

  1. Should audit logging be supported? (Redis Streams vs skip entirely)
  2. Connection pooling strategy for Redis?
  3. Cluster mode support from day one, or single-node first?
  4. Should StackOptions be backend-agnostic, or DynamoDB-specific?

Dependencies

  • #150 - Repository Protocol extraction (must be completed first)

Related

  • #49 - v1.0.0 Release (this is post-1.0.0)
  • #148 - Comparison page (will reference Redis as alternative)

Decision Log

  • 2025-01-15: Confirmed this can be done without breaking changes via optional repository parameter
  • 2025-01-15: DAX ruled out as optimization path (TransactWriteItems invalidates cache)
  • 2025-01-15: No current user demand, but keeping as option for EKS/LiteLLM integration scenarios

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with dependency #150, then inspect the existing RateLimiter, SyncRateLimiter, and Repository entry points before defining the protocol in repository_protocol.py. Review pyproject.toml for optional dependencies and the listed Redis key, Lua, and testing requirements. Done means the phased acceptance criteria are resolved, including backend behavior, tests, and documentation, but several design questions remain open.

Written by the indexing model from the issue text.

Assessment

Tech stack
docker, lua, python, redis
Domain
backend, databases, testing
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.