Skip to main content

eXtended Rate Limiter - A distributed rate limiter with multiple algorithms (Token Bucket and Fixed Window)

Project description

XRL - eXtended Rate Limiter

PyPI version Python Support License: MIT

A distributed rate limiter with multiple algorithms (Token Bucket and Fixed Window) for Python applications.

Features

  • Multiple Algorithms:
    • Token Bucket: Smooth rate limiting with burst capacity
    • Fixed Window: Simple time-window based rate limiting
  • Distributed: Works across multiple application instances using Redis
  • Async/Await Support: Built for modern Python async applications
  • Configurable: Flexible capacity and refill rates
  • Atomic Operations: Uses Redis Lua scripts for consistency
  • Auto-expiring Keys: Automatic cleanup of unused rate limit keys
  • Debug Logging: Built-in logging for monitoring rate limit operations

Installation

pip install xrl-python

Quick Start

import asyncio
import redis.asyncio as redis
from xrl import TokenBucketRateLimiter, FixedWindowRateLimiter

async def main():
    # Create Redis connection
    redis_client = redis.from_url("redis://localhost")
    
    # Choose your rate limiter implementation
    # Token Bucket (smoother rate limiting with burst capacity)
    limiter = TokenBucketRateLimiter(redis_client)
    
    # Or Fixed Window (simple time-window based)
    # limiter = FixedWindowRateLimiter(redis_client)
    
    # Acquire a token (blocks until available)
    await limiter.acquire_token("user:123", capacity=100, rate=10)
    print("✅ Request allowed!")
    
    # Try to acquire without blocking
    if await limiter.try_acquire_token("user:123", capacity=100, rate=10):
        print("✅ Token acquired immediately!")
    else:
        print("❌ Rate limited")
    
    await redis_client.aclose()

if __name__ == "__main__":
    asyncio.run(main())

Usage Examples

Token Bucket Rate Limiter

import asyncio
import redis.asyncio as redis
from xrl import TokenBucketRateLimiter

async def rate_limited_api():
    redis_client = redis.from_url("redis://localhost")
    limiter = TokenBucketRateLimiter(redis_client)
    
    try:
        # 100 requests per minute (100/60 = 1.67 tokens per second)
        await limiter.acquire_token("api:endpoint", capacity=100, rate=100/60)
        
        # Your API logic here
        return {"status": "success"}
        
    finally:
        await redis_client.aclose()

Fixed Window Rate Limiter

import asyncio
import redis.asyncio as redis
from xrl import FixedWindowRateLimiter

async def rate_limited_api():
    redis_client = redis.from_url("redis://localhost")
    limiter = FixedWindowRateLimiter(redis_client)
    
    try:
        # 100 requests per minute
        await limiter.acquire_token("api:endpoint", capacity=100, rate=100/60)
        
        # Your API logic here
        return {"status": "success"}
        
    finally:
        await redis_client.aclose()

Per-User Rate Limiting

async def user_rate_limit(user_id: str):
    redis_client = redis.from_url("redis://localhost")
    limiter = TokenBucketRateLimiter(redis_client)  # or FixedWindowRateLimiter
    
    try:
        # Different limits per user
        user_key = f"user:{user_id}"
        
        # 200 requests per minute for this user
        await limiter.acquire_token(user_key, capacity=200, rate=200/60)
        
        return process_user_request(user_id)
        
    finally:
        await redis_client.aclose()

Non-blocking Rate Limiting

async def try_process_request(request_id: str):
    redis_client = redis.from_url("redis://localhost")
    limiter = TokenBucketRateLimiter(redis_client)  # or FixedWindowRateLimiter
    
    try:
        # Try to acquire token without waiting
        if await limiter.try_acquire_token(f"request:{request_id}", capacity=50, rate=5):
            return await process_request(request_id)
        else:
            return {"error": "Rate limit exceeded", "retry_after": 1}
            
    finally:
        await redis_client.aclose()

API Reference

BaseRateLimiter Class

Base class for all rate limiter implementations.

__init__(redis_client: redis.Redis)

Initialize the rate limiter.

Parameters:

  • redis_client: An instance of redis.asyncio.Redis

async acquire_token(key: str, capacity: int, rate: float) -> bool

Acquire a token, waiting if necessary until one becomes available.

Parameters:

  • key: Unique identifier for the rate limit bucket
  • capacity: Maximum number of tokens/requests allowed
  • rate: Token refill rate or requests per second

Returns:

  • True when a token is successfully acquired

async try_acquire_token(key: str, capacity: int, rate: float) -> bool

Try to acquire a token without waiting.

Parameters:

  • key: Unique identifier for the rate limit bucket
  • capacity: Maximum number of tokens/requests allowed
  • rate: Token refill rate or requests per second

Returns:

  • True if token was acquired, False if rate limited

TokenBucketRateLimiter

Smooth rate limiting with burst capacity. Best for:

  • APIs that need to handle bursts of traffic
  • Applications requiring precise rate control
  • Scenarios where you want to allow some burst capacity

FixedWindowRateLimiter

Simple time-window based rate limiting. Best for:

  • Simple rate limiting needs
  • When burst capacity is not required
  • When you want to reset limits at fixed intervals

Rate Calculation Examples

# 100 requests per minute
rate = 100 / 60  # 1.67 tokens per second

# 500 requests per hour  
rate = 500 / 3600  # 0.139 tokens per second

# 10 requests per second
rate = 10  # 10 tokens per second

# 1 request every 5 seconds
rate = 1 / 5  # 0.2 tokens per second

Redis Configuration

XRL requires a Redis server. The rate limiter uses:

  • Keys: {your_key} and {your_key}:timestamp (Token Bucket)
  • Keys: {your_key}:{window_start} (Fixed Window)
  • TTL: Automatically set based on bucket refill time or window size
  • Memory: Minimal - only stores token count and timestamp per key

Error Handling

import redis.exceptions

async def robust_rate_limiting():
    try:
        redis_client = redis.from_url("redis://localhost")
        limiter = TokenBucketRateLimiter(redis_client)  # or FixedWindowRateLimiter
        
        await limiter.acquire_token("key", capacity=100, rate=10)
        
    except redis.exceptions.ConnectionError:
        # Handle Redis connection issues
        print("Redis connection failed")
        
    except redis.exceptions.TimeoutError:
        # Handle Redis timeout
        print("Redis operation timed out")
        
    finally:
        await redis_client.aclose()

Testing

# Install dependencies with Poetry
poetry install

# Run all tests (requires Redis server)
poetry run pytest tests/ -v

# Run only unit tests (no Redis required)
poetry run pytest tests/test_xrl.py -v

# Run only integration tests
poetry run pytest tests/test_xrl_integration.py -v

Development

This project uses Poetry for dependency management:

# Install Poetry
curl -sSL https://install.python-poetry.org | python3 -

# Install dependencies
poetry install

# Add a new dependency
poetry add package-name

# Add a development dependency
poetry add --group dev package-name

# Run commands in the Poetry environment
poetry run python your_script.py
poetry run pytest tests/

# Build the package
poetry build

License

MIT License - see LICENSE file for details.

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for your changes
  4. Ensure all tests pass
  5. Submit a pull request

Changelog

0.1.0

  • Initial release
  • Token bucket algorithm implementation
  • Async/await support
  • Redis Lua script for atomic operations

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

xrl_python-0.2.0.tar.gz (6.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

xrl_python-0.2.0-py3-none-any.whl (7.4 kB view details)

Uploaded Python 3

File details

Details for the file xrl_python-0.2.0.tar.gz.

File metadata

  • Download URL: xrl_python-0.2.0.tar.gz
  • Upload date:
  • Size: 6.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for xrl_python-0.2.0.tar.gz
Algorithm Hash digest
SHA256 106b184dcfebc01a6a6e767b7641d07ce08d0abecdeee0ba7eee30477d2611fb
MD5 60d13595e170b077889a54a0b8d8e51a
BLAKE2b-256 67a8becec05ecf1185968f694e1c672664ee6394410620c5ffdc2f92c81f19a9

See more details on using hashes here.

File details

Details for the file xrl_python-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: xrl_python-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 7.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for xrl_python-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a39cceec79b61011570e2e23464da16372a4c4426b0cc36845ce850b17af7dcc
MD5 d3e3f050e32b614b2c8f7d1887b87c3c
BLAKE2b-256 165e1f6993d89d1a192ee8f35355961dba7e1613b0d4fd327bb54956f25f0b5d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page