Skip to main content

rust-py-rate-limit

Fast local rate limiting for Python, powered by Rust.

🌐 Website: rust-py-rate-limit.vercel.app

A fast, thread-safe, in-process rate limiter for Python with a core written in Rust (via PyO3 + maturin). Use it to protect endpoints, functions, internal APIs, workers and backend scripts against bursts of traffic — with zero external services.

from rust_py_rate_limit import RateLimiter

limiter = RateLimiter(limit=10, window_seconds=60)

if limiter.allow("user:123"):
    print("allowed")
else:
    print("blocked")

Table of contents


What is this?

rust-py-rate-limit is a local (in-process) rate limiter. Every limiter instance keeps its counters in memory inside your Python process, guarded by a concurrent, sharded hash map on the Rust side. There is no Redis, no network hop, and no serialization on the hot path — just a couple of atomic operations per request.

It works anywhere Python runs:

  • Plain Python
  • FastAPI
  • Django
  • Flask (preview)
  • Background workers and scripts

Why Rust?

  • Speed — the counting logic is compiled native code; the hot path releases the GIL so multiple Python threads can check limits in parallel.
  • Safety — no data races by construction. State lives in a DashMap (a sharded concurrent map) and statistics use lock-free atomics, so there is no global lock on the critical path.
  • Simplicity — a tiny, predictable API surface that is hard to misuse.

Installation

pip install rust-py-rate-limit

Requires Python 3.10+. Wheels are published for Linux, macOS and Windows, so no Rust toolchain is needed to install.

Quick start

from rust_py_rate_limit import RateLimiter

limiter = RateLimiter(limit=3, window_seconds=60)

assert limiter.allow("ip:127.0.0.1") is True
assert limiter.allow("ip:127.0.0.1") is True
assert limiter.allow("ip:127.0.0.1") is True
assert limiter.allow("ip:127.0.0.1") is False   # limit reached

Opt into the Sliding Window algorithm to smooth bursts at the window boundary (the default is "fixed"):

limiter = RateLimiter(limit=100, window_seconds=60, algorithm="sliding")
limiter.algorithm  # "sliding"

How Fixed Window works

The default algorithm is Fixed Window. Each key gets a counter and a window start time. Within a window of window_seconds, up to limit requests are admitted; once the window elapses, the counter resets.

limit = 3, window = 60s, key = "user:1"

request 1 -> allowed
request 2 -> allowed
request 3 -> allowed
request 4 -> blocked
... 60s later ...
request 5 -> allowed   (new window)

Fixed Window is simple and cheap. Its only caveat is that it can admit up to 2 * limit requests around a window boundary (a burst at the end of one window plus a burst at the start of the next). If you need stricter smoothing, use algorithm="sliding" (below).

How Sliding Window works

Set algorithm="sliding" for the sliding window counter, which removes the boundary doubling. It keeps the count for the current aligned window plus the previous one, and weights the previous window by how much of it still overlaps the trailing window_seconds ending at now:

estimated = previous_count * weight + current_count
weight    = (window_seconds - elapsed_in_current_window) / window_seconds

A request is admitted while estimated < limit. This is O(1) in time and memory per key (unlike a sliding log, which stores every timestamp) and smooths the burst at the boundary, at the cost of being an approximation rather than an exact count.

limiter = RateLimiter(limit=10, window_seconds=60, algorithm="sliding")
# A full burst at the end of one window leaves far fewer slots right after the
# boundary, instead of a fresh `limit` as Fixed Window would.

API reference

RateLimiter(limit: int, window_seconds: int, algorithm: str = "fixed")

limit and window_seconds must be positive integers (passing 0 or a negative value raises ValueError). algorithm selects the strategy — "fixed" (default) or "sliding"; any other value raises ValueError.

Method Returns Description
allow(key: str) bool Consume one request. True if admitted, False if blocked.
check(key: str) dict Consume one request and return full detail (see below).
remaining(key: str) int Requests left in the current window without consuming one.
reset(key: str) bool Drop a key's state. True if it existed.
clear() None Drop all keys.
stats() dict Activity counters (see Statistics).
cleanup_expired() int Remove keys whose window has expired. Returns the count removed.

Read-only properties: limiter.max_requests, limiter.window_seconds and limiter.algorithm ("fixed" or "sliding"). (The configured limit is max_requests, since .limit(...) is the decorator.)

check() return value

Allowed:

{
    "allowed": True,
    "limit": 100,
    "remaining": 99,
    "reset_after_seconds": 60,
    "retry_after_seconds": 0,
}

Blocked:

{
    "allowed": False,
    "limit": 100,
    "remaining": 0,
    "reset_after_seconds": 42,
    "retry_after_seconds": 42,
}

FastAPI

Manual check

from fastapi import FastAPI, Request, HTTPException
from rust_py_rate_limit import RateLimiter

app = FastAPI()
limiter = RateLimiter(limit=100, window_seconds=60)

@app.get("/api/users")
def list_users(request: Request):
    key = request.client.host
    if not limiter.allow(key):
        raise HTTPException(status_code=429, detail="Too many requests")
    return {"users": []}

Middleware

from rust_py_rate_limit.fastapi import RateLimitMiddleware

app.add_middleware(
    RateLimitMiddleware,
    limit=100,
    window_seconds=60,
    key_func=lambda request: request.client.host,
)

When a request is blocked the middleware responds with 429 and {"detail": "Too many requests"}. Every response carries the standard headers:

X-RateLimit-Limit
X-RateLimit-Remaining
X-RateLimit-Reset
Retry-After      (only when blocked)

Django

# settings.py
MIDDLEWARE = [
    # ...
    "rust_py_rate_limit.django.RateLimitMiddleware",
]

RUST_PY_RATE_LIMIT = {
    "LIMIT": 100,
    "WINDOW_SECONDS": 60,
    "KEY": "ip",  # "ip" or "user"
}

Or check manually in a view:

from django.http import JsonResponse
from rust_py_rate_limit import RateLimiter

limiter = RateLimiter(limit=100, window_seconds=60)

def my_view(request):
    key = request.META.get("REMOTE_ADDR")
    if not limiter.allow(key):
        return JsonResponse({"detail": "Too many requests"}, status=429)
    return JsonResponse({"ok": True})

Flask

from flask import Flask
from rust_py_rate_limit.flask import FlaskRateLimiter

app = Flask(__name__)
limiter = FlaskRateLimiter(app, limit=100, window_seconds=60)

@app.get("/api/users")
@limiter.limit()
def list_users():
    return {"users": []}

Decorator

from rust_py_rate_limit import RateLimiter, RateLimitExceeded

limiter = RateLimiter(limit=5, window_seconds=60)

@limiter.limit("login")
def login():
    return "ok"

When the limit is exceeded the decorated function raises RateLimitExceeded (which carries .key, .limit and .retry_after). The key may also be a callable that derives the key from the function's arguments:

@limiter.limit(lambda user_id: f"user:{user_id}")
def fetch(user_id):
    ...

Statistics

limiter.stats()
# {
#     "allowed": 1200,
#     "blocked": 35,
#     "total_checks": 1235,
#     "active_keys": 20,
# }

Limitations

Be honest with yourself about what an in-process limiter can and cannot do:

  • The rate-limit state is local to the process.
  • Under Gunicorn/Uvicorn with multiple workers, each worker keeps its own counters, so the effective global limit is roughly limit × workers.
  • It is not a replacement for Redis when you need distributed rate limiting.
  • Fixed Window can allow short bursts at the boundary between two windows; use algorithm="sliding" to smooth them.
  • For distributed production setups, a Redis/Postgres backend is planned (see the roadmap).

Roadmap

Version Highlights Status
v0.1.0 Fixed Window · allow/check/remaining/reset/clear/stats/cleanup_expired · pytest · README
v0.1.5 Decorator · FastAPI/Django/Flask middleware · HTTP headers
v0.2.0 Sliding Window (algorithm="sliding")
v0.3.0 Token Bucket · background cleanup 🔜
v0.4.0 Redis backend · distributed rate limiting 🔜
v0.5.0 Prometheus metrics · ImmutableLog integration 🔜

Development

# Rust unit tests
cargo test

# Build the extension into a virtualenv and run the Python tests
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"      # or: pip install maturin && maturin develop
maturin develop
pytest

License

MIT © Roberto Lima

Download files

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

Source Distribution

rust_py_rate_limit-0.2.0.tar.gz (31.8 kB view details)

Uploaded Source

Built Distributions

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

rust_py_rate_limit-0.2.0-cp312-cp312-win_amd64.whl (154.9 kB view details)

Uploaded CPython 3.12Windows x86-64

rust_py_rate_limit-0.2.0-cp312-cp312-manylinux_2_34_x86_64.whl (276.9 kB view details)

Uploaded CPython 3.12manylinux: glibc 2.34+ x86-64

rust_py_rate_limit-0.2.0-cp312-cp312-macosx_11_0_arm64.whl (247.5 kB view details)

Uploaded CPython 3.12macOS 11.0+ ARM64

File details

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

File metadata

  • Download URL: rust_py_rate_limit-0.2.0.tar.gz
  • Upload date:
  • Size: 31.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for rust_py_rate_limit-0.2.0.tar.gz
Algorithm Hash digest
SHA256 2f2818e0f2953d6e30345d68f8ae8d7378f1e76a8bd1787d4c8de2d5cad06bbd
MD5 a367839729784fc9d8a1a4d7a3bbaea8
BLAKE2b-256 98d04e7ea06318551b1aeb08e23a31cbf594f962270ec91653f3629a5fc865b4

See more details on using hashes here.

Provenance

The following attestation bundles were made for rust_py_rate_limit-0.2.0.tar.gz:

Publisher: release.yml on robertolima-dev/rust-py-rate-limit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file rust_py_rate_limit-0.2.0-cp312-cp312-win_amd64.whl.

File metadata

File hashes

Hashes for rust_py_rate_limit-0.2.0-cp312-cp312-win_amd64.whl
Algorithm Hash digest
SHA256 1b45d47d6adaa0cac0d6297cf166e72e2dced8972e5a243bd2191e57d5154745
MD5 e9cec39e8fe065375ac94eb3db22fe5e
BLAKE2b-256 773d88aa2093f4ac4b7b952c98b025b05e71f6e5be666d6642a6651eeaee6659

See more details on using hashes here.

Provenance

The following attestation bundles were made for rust_py_rate_limit-0.2.0-cp312-cp312-win_amd64.whl:

Publisher: release.yml on robertolima-dev/rust-py-rate-limit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file rust_py_rate_limit-0.2.0-cp312-cp312-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for rust_py_rate_limit-0.2.0-cp312-cp312-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 b03eb221c6ad5ef44a0505eebaa25930533f8b0ebbde144aef66ccdf62dc8ed7
MD5 c8c910f85693cc0ff6b3b56a29ed4d2d
BLAKE2b-256 90979fb3d9cc4722ecd37574adfd7cadb8cb179e204eab5a96ef45511c3a57b2

See more details on using hashes here.

Provenance

The following attestation bundles were made for rust_py_rate_limit-0.2.0-cp312-cp312-manylinux_2_34_x86_64.whl:

Publisher: release.yml on robertolima-dev/rust-py-rate-limit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file rust_py_rate_limit-0.2.0-cp312-cp312-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for rust_py_rate_limit-0.2.0-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 c3cd9cc2166661fc8b423fac2dfe076b99b8331156a8c542194474a2227978db
MD5 442f9815153dc396b14529283171035f
BLAKE2b-256 94846a372a9f38dbdb25553f0bb43244738ee2da01d380a3af24d5fc390c7e9f

See more details on using hashes here.

Provenance

The following attestation bundles were made for rust_py_rate_limit-0.2.0-cp312-cp312-macosx_11_0_arm64.whl:

Publisher: release.yml on robertolima-dev/rust-py-rate-limit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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