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",
    max_keys: int | None = None,
    cleanup_interval: float | None = None,
)

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. max_keys and cleanup_interval bound memory — see Bounded memory.

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, limiter.algorithm ("fixed" or "sliding"), limiter.max_keys and limiter.cleanup_interval. (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):
    ...

Bounded memory

The key is usually a client IP or token — something the caller controls. An unbounded limiter lets a client that mints a fresh key per request grow the map until the process runs out of memory: the library meant to protect you becomes the attack vector. Two options close that:

limiter = RateLimiter(
    limit=100,
    window_seconds=60,
    max_keys=100_000,      # hard cap on tracked keys
    cleanup_interval=60,   # background thread drops expired keys every 60s
)
  • max_keys — when the map is full, expired keys are reclaimed first (at most once per second, so a flood can't make every request pay a full sweep). If it is still full, the new key is rejected (allowed=False, retry_after_seconds=window_seconds) and counted in stats()["rejected_full"]. Keys already tracked are never evicted to make room — evicting would reset a legitimate client's counter on the attacker's behalf.
  • cleanup_interval — a native thread (no GIL) that calls cleanup_expired() on that period. It holds only a weak reference, so it stops on its own when the limiter is garbage-collected.

Both default to None, which keeps the pre-0.3 behaviour (unbounded, manual cleanup). For anything keyed by client input, set both. The FastAPI middleware and FlaskRateLimiter accept the same two keyword arguments; on Django, use "MAX_KEYS" and "CLEANUP_INTERVAL" in RUST_PY_RATE_LIMIT.

The cap is checked without a global lock, so under heavy concurrency the map can overshoot max_keys by at most the number of threads inserting new keys at that same instant.

Statistics

limiter.stats()
# {
#     "allowed": 1200,
#     "blocked": 35,
#     "total_checks": 1235,
#     "active_keys": 20,
#     "rejected_full": 0,   # new keys refused by max_keys (also in "blocked")
# }

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 max_keys (bounded memory) · background cleanup (cleanup_interval) ✅
v0.3.x Token Bucket 🔜
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

Metadata

Release files for rust-py-rate-limit 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 rust-py-rate-limit 0.3.0
File Size Uploaded Provenance Actions
rust_py_rate_limit-0.3.0.tar.gz 45.4 kB With provenance Details for rust_py_rate_limit-0.3.0.tar.gz

Built distributions (wheels)

Table of built distributions (wheels) for rust-py-rate-limit 0.3.0
File Actions
rust_py_rate_limit-0.3.0-cp310-abi3-win_amd64.whl CPython 3.10 abi3 Windows x86-64 Details for rust_py_rate_limit-0.3.0-cp310-abi3-win_amd64.whl
rust_py_rate_limit-0.3.0-cp310-abi3-musllinux_1_2_x86_64.whl CPython 3.10 abi3 Linux musl 1.2+ x86-64 Details for rust_py_rate_limit-0.3.0-cp310-abi3-musllinux_1_2_x86_64.whl
rust_py_rate_limit-0.3.0-cp310-abi3-musllinux_1_2_aarch64.whl CPython 3.10 abi3 Linux musl 1.2+ ARM64 Details for rust_py_rate_limit-0.3.0-cp310-abi3-musllinux_1_2_aarch64.whl
rust_py_rate_limit-0.3.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.10 abi3 Linux glibc 2.17+ x86-64 Details for rust_py_rate_limit-0.3.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
rust_py_rate_limit-0.3.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.10 abi3 Linux glibc 2.17+ ARM64 Details for rust_py_rate_limit-0.3.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
rust_py_rate_limit-0.3.0-cp310-abi3-macosx_11_0_arm64.whl CPython 3.10 abi3 macOS 11.0+ ARM64 Details for rust_py_rate_limit-0.3.0-cp310-abi3-macosx_11_0_arm64.whl
rust_py_rate_limit-0.3.0-cp310-abi3-macosx_10_12_x86_64.whl CPython 3.10 abi3 macOS 10.12+ x86-64 Details for rust_py_rate_limit-0.3.0-cp310-abi3-macosx_10_12_x86_64.whl

Total release size: 2.4 MB

Release files / rust_py_rate_limit-0.3.0.tar.gz

Download URL rust_py_rate_limit-0.3.0.tar.gz
Size 45.4 kB
Tags Source
SHA-256 checksum
How to use checksums
0ca0d95973130dae7327cfb6b2dae739f47c565e0272a4508874421e15ee9c68
BLAKE2b-256 checksum
How to use checksums
947c476886b1567e21d04f688922556fd52e214916ebf9f6b8d1f219e8143aad
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 20, 2026.

Transparency log

Release files / rust_py_rate_limit-0.3.0-cp310-abi3-win_amd64.whl

Download URL rust_py_rate_limit-0.3.0-cp310-abi3-win_amd64.whl
Size 185.0 kB
Tags CPython 3.10 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
12c7efac6b2d4c94741fe0aee59be1e6e607dd8fe8bd5a7511ceabeb5032ee7c
BLAKE2b-256 checksum
How to use checksums
5990ca8dde56748c8e84eba73190c2ff49cd2a471a2f0be217e0a84cc77acdc1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 20, 2026.

Transparency log

Release files / rust_py_rate_limit-0.3.0-cp310-abi3-musllinux_1_2_x86_64.whl

Download URL rust_py_rate_limit-0.3.0-cp310-abi3-musllinux_1_2_x86_64.whl
Size 515.0 kB
Tags CPython 3.10 Linux musl 1.2+ x86-64 abi3
SHA-256 checksum
How to use checksums
6b68c50426a95cc82731677dc0f764ccac1ac2c56a2f2ae63758a4ff101cdabc
BLAKE2b-256 checksum
How to use checksums
529cbcf5b61c56fb057f79149b52395b5b47bb7e9c1adfdd734ac5c03ad4c69a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 20, 2026.

Transparency log

Release files / rust_py_rate_limit-0.3.0-cp310-abi3-musllinux_1_2_aarch64.whl

Download URL rust_py_rate_limit-0.3.0-cp310-abi3-musllinux_1_2_aarch64.whl
Size 479.5 kB
Tags CPython 3.10 Linux musl 1.2+ ARM64 abi3
SHA-256 checksum
How to use checksums
b95408509a47cc4e1d5934fcc762cd43f85dbf13993ef2654bb0aa7c1ea022a2
BLAKE2b-256 checksum
How to use checksums
7421cca64123cb3a1a20c4a3689e51e0ce02d85f49a96430c6a36b6476ff7366
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 20, 2026.

Transparency log

Release files / rust_py_rate_limit-0.3.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL rust_py_rate_limit-0.3.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 304.0 kB
Tags CPython 3.10 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
6efc8dc72f200eb733dccbbca1f7468e596ab53682193b3287d6afee1732632a
BLAKE2b-256 checksum
How to use checksums
68953ba1802838c6d279ab2188f9b7087da434e513cb98c1e2cc19f42b643bad
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 20, 2026.

Transparency log

Release files / rust_py_rate_limit-0.3.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL rust_py_rate_limit-0.3.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 302.1 kB
Tags CPython 3.10 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
c6889f82b6dcaaf33b7531d821d8ed3dd0a0b251735ac034c99f13fdedff8801
BLAKE2b-256 checksum
How to use checksums
ff2f38d10bf787426a0ef0ad3ab0b9b961da62dca61f43465ea76dbf8eec0db7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 20, 2026.

Transparency log

Release files / rust_py_rate_limit-0.3.0-cp310-abi3-macosx_11_0_arm64.whl

Download URL rust_py_rate_limit-0.3.0-cp310-abi3-macosx_11_0_arm64.whl
Size 271.5 kB
Tags CPython 3.10 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
de8f20fe1dcefce1bca246a0922d591edd5fb957d38be8d1081ad9da1e5ebe50
BLAKE2b-256 checksum
How to use checksums
d2e7a8c39d2c37fef3a81429b53dc9e56b4d98af175914454911abc98e9df224
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 20, 2026.

Transparency log

Release files / rust_py_rate_limit-0.3.0-cp310-abi3-macosx_10_12_x86_64.whl

Download URL rust_py_rate_limit-0.3.0-cp310-abi3-macosx_10_12_x86_64.whl
Size 280.3 kB
Tags CPython 3.10 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
bc7de155c0095ebb00aae74c824e2e8e06258be31eee5beb27fc11100420611d
BLAKE2b-256 checksum
How to use checksums
82b56692705a7a6a01ba11c188c0e78b7ed2bdf3e447d6cfcb37b5282779d4d0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 20, 2026.

Transparency log

Security for rust-py-rate-limit 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 8 files in this release have been uploaded with provenance metadata.

Select from 8 files

Attestation for rust_py_rate_limit-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 Oct 20, 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