Skip to main content

Moderato

In musical notation, moderato means "at a moderate pace." Moderato enforces your API's tempo.

Python Version License Redis Code Style

A Redis-backed rate limiting library for async Python applications.

Features | Quick Start | Algorithms | Documentation | Examples


What is Moderato?

Moderato is a Redis-backed rate limiting library for async Python applications. It provides three algorithms, FastAPI integration, optional Prometheus metrics, cost-based limits, and tenant isolation.

Use cases:

  • FastAPI applications requiring rate limiting
  • Multi-tenant SaaS platforms with tier-based limits
  • APIs that need Redis-backed limits across multiple application instances
  • Services that need Prometheus counters and latency histograms
  • Applications requiring atomic rate limit decisions

Features

Core Capabilities

  • Three Algorithms - Fixed Window, Token Bucket & Sliding Window
  • Async-first design - Built for FastAPI and modern async Python
  • Atomic decisions - Redis Lua scripts keep checks and updates in one operation
  • Redis server time - Consistent windows across application instances
  • Integer precision - Uses integer math (x1000 multiplier) for accuracy

Integrations and controls

  • Rate limit headers - Standard headers on decorated FastAPI responses
  • Prometheus Metrics - Optional counters and latency histograms
  • Multi-tenant support - Isolated limits for different users/tiers/organizations
  • Decorator-based API - Clean, declarative rate limiting
  • Cost-based limiting - Weight expensive operations appropriately
  • Flexible configuration - Customizable key extraction, algorithms, and costs

Quick Start

Installation

pip install moderato

# With FastAPI/Starlette middleware integration
pip install 'moderato[fastapi]'

# With metrics support (optional)
pip install 'moderato[metrics]'

# Everything (optional)
pip install 'moderato[all]'

Basic Example

from contextlib import asynccontextmanager

from fastapi import FastAPI, Request
from moderato import RateLimiter, RateLimitHeadersMiddleware

limiter = RateLimiter(redis_url="redis://localhost:6379")

@asynccontextmanager
async def lifespan(app: FastAPI):
    await limiter.connect()
    yield
    await limiter.close()

app = FastAPI(lifespan=lifespan)
app.add_middleware(RateLimitHeadersMiddleware)

@app.get("/api/users")
@limiter.limit("100/minute")
async def get_users(request: Request):
    return {"users": ["Alice", "Bob"]}

This gives you:

  • Rate limiting (100 requests/minute per IP)
  • Rate limit headers on this decorated endpoint
  • Proper 429 responses when exceeded
  • Redis-backed, distributed-ready

Algorithms

Moderato provides three tested algorithms. Choose based on the traffic behavior you want:

Fixed Window (Default)

Best for: Simple rate limiting, strict per-window limits, lower memory usage

@limiter.limit("100/minute", algorithm="fixed_window")
async def endpoint(request: Request):
    return {"data": "..."}

How it works:

  • Time divided into fixed windows (e.g., 14:35:00 - 14:36:00)
  • Counter increments per request
  • Resets when window expires

Pros: Simple, low memory, strict limits
Cons: Possible boundary bursts (can get 2x at window edge)

Token Bucket

Best for: Smoothing bursts while allowing capacity to refill continuously

@limiter.limit("100/minute", algorithm="token_bucket")
async def endpoint(request: Request):
    return {"data": "..."}

How it works:

  • Bucket holds tokens (capacity = 100)
  • Tokens refill continuously (~1.67/second for 100/minute)
  • Each request consumes tokens

Pros: Continuous refill and configurable burst capacity Cons: State uses a Redis hash and allows bursts up to bucket capacity

Sliding Window

Best for: Approximating a rolling window without storing every request timestamp

@limiter.limit("100/minute", algorithm="sliding_window")
async def endpoint(request: Request):
    return {"data": "..."}

How it works:

  • Combines current window with weighted portion of previous window
  • Provides smooth transition between windows
  • Approximates a rolling count with two fixed counters

Pros: Smooths fixed-window boundaries with constant Redis storage Cons: It is an approximation rather than an exact request log

Algorithm Comparison

Feature Fixed Window Token Bucket Sliding Window
Simplicity High Medium Medium
Boundary Bursts Possible (2x) None None
Redis data String counter Hash with tokens and timestamp Two string counters
Traffic behavior Resets at boundaries Continuous refill Weighted window transition
Accuracy model Exact fixed window Exact token bucket state Approximate rolling window

Recommendation: Choose fixed window for simple quotas, token bucket for controlled bursts, and sliding window counter when fixed-window boundary bursts are undesirable.


Documentation

Configuration

from moderato import RateLimiter

limiter = RateLimiter(
    redis_url="redis://localhost:6379",      # Redis connection URL
    key_prefix="myapp:ratelimit",            # Prefix for Redis keys
    default_algorithm="token_bucket",        # Algorithm: "fixed_window", "token_bucket", or "sliding_window"
    enable_metrics=False,                    # Enable Prometheus metrics
)
Parameter Type Default Description
redis_url str "redis://localhost:6379" Redis connection string
key_prefix str "ratelimit" Prefix for all Redis keys
default_algorithm str "fixed_window" Default algorithm to use
enable_metrics bool False Enable Prometheus metrics collection

Rate Limit Formats

"10/second"   # 10 requests per second
"100/minute"  # 100 requests per minute
"1000/hour"   # 1000 requests per hour
"10000/day"   # 10000 requests per day

Decorator API

Basic IP-based limiting

@app.get("/api/data")
@limiter.limit("100/minute")
async def get_data(request: Request):
    return {"data": "..."}

Custom key extraction

@app.get("/api/user/{user_id}")
@limiter.limit(
    "1000/hour",
    key=lambda req: f"user:{req.path_params.get('user_id')}"
)
async def get_user_data(request: Request, user_id: str):
    return {"user_id": user_id}

Choose algorithm

@app.get("/api/smooth")
@limiter.limit("100/minute", algorithm="token_bucket")
async def smooth_endpoint(request: Request):
    return {"data": "..."}

Share a limit across routes

Decorated endpoints use separate method-and-route scopes by default. Set scope to share one bucket:

@limiter.limit("100/minute", scope="search-api")
async def search(request: Request):
    ...

Manual check() calls use the "global" scope unless you pass one explicitly.

Automatic Headers

For endpoints using @limiter.limit(...), add the middleware to inject rate limit headers:

from moderato import RateLimitHeadersMiddleware

app.add_middleware(RateLimitHeadersMiddleware)

Headers added to decorated responses:

  • X-RateLimit-Limit: Maximum requests allowed
  • X-RateLimit-Remaining: Requests remaining in current window
  • X-RateLimit-Reset: Unix timestamp when the limit resets

Additional headers on 429 responses:

  • Retry-After: Seconds to wait before retrying

Prometheus Metrics

Install moderato[metrics], then enable collection:

limiter = RateLimiter(
    redis_url="redis://localhost:6379",
    enable_metrics=True  # Enable Prometheus metrics
)

Metrics collected:

  • moderato_checks_total - Total rate limit checks
  • moderato_check_duration_seconds - Check latency histogram
  • moderato_limit_exceeded_total - Rate limit violations
  • moderato_backend_operations_total - Redis operations

Expose metrics endpoint:

from prometheus_client import generate_latest, CONTENT_TYPE_LATEST

@app.get("/metrics")
async def metrics():
    return Response(content=generate_latest(), media_type=CONTENT_TYPE_LATEST)

Multi-Tenant Setup

# Define tier-specific limits
TIER_LIMITS = {
    "free": "100/hour",
    "premium": "1000/hour",
    "enterprise": "10000/hour",
}

@app.get("/api/data")
async def get_data(request: Request):
    api_key = request.headers.get("X-API-Key", "anonymous")
    tier = get_user_tier(api_key)
    await limiter.check(
        key=api_key,
        rate=TIER_LIMITS[tier],
        tenant_type=tier,
        scope="GET:/api/data",
    )
    return {"data": "..."}

Cost-Based Limiting

@app.post("/api/ml/inference")
@limiter.limit(
    "100/minute",
    cost=lambda req: 10  # This endpoint counts as 10 regular requests
)
async def ml_inference(request: Request):
    return {"prediction": "..."}

Error Handling

from moderato import RateLimitExceeded

@app.exception_handler(RateLimitExceeded)
async def rate_limit_handler(request: Request, exc: RateLimitExceeded):
    return JSONResponse(
        status_code=429,
        content={
            "error": "Rate limit exceeded",
            "retry_after": exc.retry_after,
            "limit": exc.limit,
        },
        headers={"Retry-After": str(exc.retry_after)},
    )

Manual Checking

# Direct rate limit check
try:
    await limiter.check(key="user:123", rate="100/minute")
    # Request allowed
except RateLimitExceeded as e:
    # Rate limited
    print(f"Retry after {e.retry_after} seconds")

# Get usage statistics
usage = await limiter.get_usage(key="user:123", rate="100/minute")
print(f"Current: {usage['current']}, Remaining: {usage['remaining']}")

# Reset rate limit
await limiter.reset(key="user:123")

Performance

Run the benchmark suite against a local Redis instance rather than relying on hardware-independent throughput claims:

docker-compose -f docker-compose.dev.yml up -d
poetry install --with benchmarks
poetry run python benchmarks/performance.py --quick

The quick run reports throughput, latency percentiles, algorithm comparisons, and rate-limit accuracy. Run without --quick to include concurrent-client, Redis memory, and multi-tenant benchmarks. Results depend on Redis placement, network latency, hardware, Python version, and concurrency, so publish those details with any result.

Implemented optimizations:

  • Cached Lua scripts with EVALSHA and EVAL fallback
  • Redis connection pooling
  • One atomic script call per rate limit decision
  • Bounded, component-level hashing for long identifiers and scopes

Examples

See the examples/ directory:

Running Examples

# Start Redis
docker-compose -f docker-compose.dev.yml up -d

# FastAPI demo
poetry run uvicorn examples.fastapi_app:app --reload

# Multi-tenant demo
poetry run uvicorn examples.multi_tenant:app --reload --port 8001

# Algorithm comparison
poetry run python examples/algorithms_demo.py

Architecture

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   FastAPI   │────▶│ RateLimiter │────▶│    Redis    │
│     App     │     │  (Python)   │     │   (Lua)     │
└─────────────┘     └─────────────┘     └─────────────┘
                            │
                    ┌───────┴───────┐
                    │               │
            ┌───────▼─────┐ ┌───────▼─────┐
            │   Fixed     │ │   Token     │
            │   Window    │ │   Bucket    │
            └─────────────┘ └─────────────┘

For internals, read the algorithm implementations in moderato/algorithms/ and the atomic Lua scripts in moderato/scripts/ — both are heavily tested (tests/) and intentionally compact.


Development

Prerequisites

  • Python 3.9–3.13
  • Redis 7.0+
  • Poetry

Setup

git clone https://github.com/Arjun-Aravind/moderato.git
cd moderato

poetry install
docker-compose -f docker-compose.dev.yml up -d
poetry run pytest

Commands

make test          # Run tests
make test-cov      # Run tests with coverage
make lint          # Run linting
make format        # Format code
make demo          # Run algorithm demo

Testing

The test suite covers:

  • All three algorithms (Fixed Window, Token Bucket, Sliding Window)
  • Concurrent requests and race conditions
  • Multi-tenant isolation
  • Cost-based rate limiting
  • Headers middleware
  • Edge cases and error handling
# Run all tests
make test

# Run specific test file
poetry run pytest tests/test_token_bucket.py -v

# Run with coverage
make test-cov

Roadmap

  • Fixed Window algorithm
  • Token Bucket algorithm
  • Sliding Window algorithm
  • Automatic rate limit headers
  • Prometheus metrics
  • Multi-tenant support
  • Cost-based rate limiting
  • Circuit breaker pattern
  • Redis Cluster support
  • Web dashboard
  • Django integration

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.


License

This project is licensed under the MIT License - see the LICENSE file for details.

Metadata

Release files for moderato 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for moderato 0.3.0
File Size Uploaded Provenance
moderato-0.3.0.tar.gz 43.6 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for moderato 0.3.0
File Interpreter ABI Platform
moderato-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 91.4 kB

Release files / moderato-0.3.0.tar.gz

Download URL moderato-0.3.0.tar.gz
Size 43.6 kB
Tags Source
SHA-256 checksum
How to use checksums
1700f128612adff5f18aaf2daf694bb92da0e7db7d2282b983b5e3d0cc0c7e2c
BLAKE2b-256 checksum
How to use checksums
700fdd19ac3446c6dec148cb3c79895df301d613927196ad84131f04c77cf08a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release files / moderato-0.3.0-py3-none-any.whl

Download URL moderato-0.3.0-py3-none-any.whl
Size 47.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
be0979b991d8bc6675f038803ecd5a6f4b067e28e86b7b35d07b08f7204fb94f
BLAKE2b-256 checksum
How to use checksums
4364e18149f6b17ece3abcc697fac979068b7d9e1e213462680d0af9b11af4e0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Security for moderato 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for moderato-0.3.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page