Skip to main content

Metered ⏱️

Python Version License: MIT Code style: black

Metered is an enterprise-grade, asynchronous rate limiting and quota management library for Python. Designed for modern SaaS architectures, it supports FastAPI and Flask natively, offering highly precise algorithms, dynamic LLM token costing, and robust distributed state management via Redis.


🚀 Features

  • 5 Core Rate Limiting Algorithms:
    • Token Bucket (Smooth bursting)
    • Sliding Window Log (High precision, dynamic costs)
    • Sliding Window Counter (Memory-efficient approximation)
    • Fixed Window (Standard quota tracking)
    • Leaky Bucket (Strict egress shaping)

📊 Detailed Comparison of the Algorithms

Algorithm Traffic Handling Memory Usage Implementation Complexity Best Used For
Token Bucket Allows controlled bursts Low (stores 2 numbers per client) Moderate General-purpose APIs
Leaky Bucket Enforces a smooth, fixed output rate Low (bounded queue size) Moderate Data egress or traffic shaping
Fixed Window Counter Prone to boundary bursts Minimal (1 integer counter) Very Low Simple internal or low-scale apps
Sliding Window Log Perfectly accurate Very High (stores every timestamp) High High-security endpoints (e.g., login, MFA)
Sliding Window Counter Approximated accuracy, smooths boundaries Low (stores 2 adjacent window counts) Moderate Large distributed systems
  • Dynamic Costing: Perfect for GenAI/LLM wrappers—calculate the cost (tokens) of a request dynamically at runtime.
  • Quota Persistence: Define limits like "10,000 tokens per month" that survive app restarts and synchronize globally via Redis.
  • Event-Driven Architecture: Native webhook/event dispatcher with DLQ (Dead Letter Queue), exponential backoff, and strict event throttling to prevent spam when quotas are breached.
  • Backend Agnostic: Ships with a thread-safe InMemoryBackend for development and an atomic, Lua-powered RedisBackend for high-throughput production.
  • Async Native: Built with asyncio from the ground up, guaranteeing non-blocking behavior.

📦 Installation

pip install metered

(Coming soon to PyPI)


⚡ Quick Start

FastAPI Example

from fastapi import FastAPI, Request
from metered import Metered, Strategy, IdentifierType

app = FastAPI()
# By default, uses the lightweight InMemoryBackend
meter = Metered()

# Helper to identify users
def get_client_ip(req: Request) -> str:
    return req.client.host if req.client else "127.0.0.1"

# Restrict to 5 requests per 10 seconds per IP
@app.get("/api/data")
@meter.limit(
    max_tokens=5, 
    period=10, 
    strategy=Strategy.FIXED_WINDOW, 
    identifier=get_client_ip
)
async def get_data(request: Request):
    return {"data": "Success!"}

Flask Example

from flask import Flask, request, jsonify
from metered import Metered, Strategy

app = Flask(__name__)
meter = Metered()

def get_client_ip(req):
    return req.remote_addr or "127.0.0.1"

@app.route("/api/data", methods=["GET"])
@meter.limit(max_tokens=5, period=10, strategy=Strategy.FIXED_WINDOW, identifier=get_client_ip)
def get_data():
    return jsonify({"data": "Here is your data!"})

🧠 Core Concepts

Stacking Decorators

You can combine multiple constraints on a single endpoint. Metered evaluates them top-down (or as passed) and enforces the strictest rule.

@app.get("/search")
@meter.quota(plan_name="pro_plan", identifier=get_user_id)  # Evaluated first
@meter.limit(max_tokens=100, period=60, strategy=Strategy.SLIDING_WINDOW, identifier=get_user_id) # Sustained limit
@meter.limit(max_tokens=10, period=1, strategy=Strategy.SLIDING_WINDOW, identifier=get_user_id)   # Burst limit
async def search(request: Request):
    return {"results": []}

Dynamic Costs (LLMs / AI Apps)

Unlike standard rate limiters where 1 Request = 1 Token, metered allows you to define a cost function.

async def calculate_llm_cost(req: Request) -> int:
    body = await req.json()
    prompt = body.get("prompt", "")
    # e.g., 1 word = ~1.3 tokens
    return max(1, int(len(prompt.split()) * 1.3))

@app.post("/v1/completions")
@meter.limit(
    max_tokens=5000, 
    period=60, 
    strategy=Strategy.TOKEN_BUCKET, 
    identifier=get_api_key, 
    cost=calculate_llm_cost
)
async def generate_text(request: Request):
    return {"text": "AI response..."}

🏭 Production Guide

Using Redis (Recommended for Production)

For distributed systems and multi-worker deployments (e.g., Uvicorn/Gunicorn), use the RedisBackend. It relies entirely on atomic Lua scripts to prevent race conditions.

from redis.asyncio import Redis
from metered import Metered, RedisBackend

redis_client = Redis.from_url("redis://localhost:6379", decode_responses=True)
redis_backend = RedisBackend(redis_client)

meter = Metered(backend=redis_backend, quota_backend=redis_backend)

Quota Engine & Plans

Unlike short-lived rate limits, Quotas are persistent billing boundaries (e.g., Monthly API usage).

# During your app startup phase
@app.on_event("startup")
async def startup():
    # Store a persistent quota plan into the backend
    await meter.quotas.set_plan(
        target="user_123",
        plan_name="pro_plan",
        limit=100000,               # 100k tokens
        reset_period="monthly",     # resets on the 1st of every month
        identifier_type=IdentifierType.USER_ID
    )

Event Dispatcher (Alerts & Webhooks)

Metered includes an advanced event dispatcher with two-phase commits, throttling, and a DLQ (Dead Letter Queue) to reliably notify you (e.g. via Slack) when users approach their limits without spamming your network.

# 1. Configure Persistent Outbox
meter.events.configure(
    backend=redis_backend, 
    persist=True, 
    cooldown_seconds=3600 # Only alert once per hour per user
)

# 2. Start the Background Worker
asyncio.create_task(meter.events.start_worker())

# 3. Listen to Quota Warnings
@meter.events.on_quota_warning(threshold=0.8)
async def handle_quota_warning(target: str, plan_name: str, usage_ratio: float, remaining: int):
    # This handler will be retried with exponential backoff if it fails
    await send_slack_alert(f"User {target} is at {usage_ratio*100}% of their {plan_name} plan.")

📊 Performance & Benchmarks

Metered is built to sustain massive concurrency. Below is a benchmark result using locust against the fastapi_saas.py example (which includes dynamic Redis Quotas, Rate Limits, and Event Dispatching):

Type     Name                                           # reqs      # fails |    Avg     Min     Max    Med |   req/s  failures/s
--------|---------------------------------------------------------------------------------------------------|--------|-----------
GET      /premium-data                                    7865     0(0.00%) |    104       2     349    100 |  530.32        0.00

(Tested with 100 concurrent headless users. Zero failures or unhandled exceptions).


🌍 Global Middleware

If you want to apply limits globally rather than per-route, metered exports standard middlewares.

FastAPI:

from metered.integrations.fastapi import MeteredMiddleware
from metered import Limit, Strategy

# Limits all traffic globally to 100 req/sec
app.add_middleware(
    MeteredMiddleware, 
    meter=meter, 
    limits=[Limit(max_tokens=100, period=1, strategy=Strategy.FIXED_WINDOW)]
)

🤝 Contributing

Contributions are highly welcomed! Please check our open issues.

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/AmazingFeature)
  3. Commit your Changes (git commit -m 'Add some AmazingFeature')
  4. Run static checks (mypy metered/)
  5. Push to the Branch (git push origin feature/AmazingFeature)
  6. Open a Pull Request

📄 License

Distributed under the MIT License. See LICENSE for more information.

Download files

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

Source Distribution

metered-1.1.0.tar.gz (21.6 kB view details)

Uploaded Source

Built Distribution

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

metered-1.1.0-py3-none-any.whl (21.4 kB view details)

Uploaded Python 3

File details

Details for the file metered-1.1.0.tar.gz.

File metadata

  • Download URL: metered-1.1.0.tar.gz
  • Upload date:
  • Size: 21.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.14

File hashes

Hashes for metered-1.1.0.tar.gz
Algorithm Hash digest
SHA256 41fdb3765751ab496a6a4a2daea8e17f196fc16d46440aea76c76ecbd12f266a
MD5 afdc006e62e997dfb31880a6944890a4
BLAKE2b-256 3023fe067349393960be64aa58309cd55ab85a39b786002e4088794b6ea7e387

See more details on using hashes here.

File details

Details for the file metered-1.1.0-py3-none-any.whl.

File metadata

  • Download URL: metered-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 21.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.14

File hashes

Hashes for metered-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4d9993f06bb08d5135e25a0b6a3cbbf791114f5fe41dc146b7121113b1880ee0
MD5 15643fdf825705b969fd4d78465c5059
BLAKE2b-256 8536b83fb50ba38f5f4a19adad26d0d4bd1f85e1bb202ef5bf1c65a730d88a43

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.1.0 This release

2 files

1.0.0

2 files

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