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?
- Why Rust?
- Installation
- Quick start
- How Fixed Window works
- How Sliding Window works
- API reference
- FastAPI
- Django
- Flask
- Decorator
- Bounded memory
- Statistics
- Limitations
- Roadmap
- Development
- License
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 instats()["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 callscleanup_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)
| 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)
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 logRelease 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 logRelease 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 logRelease 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 logRelease 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 logRelease 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 logRelease 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 logRelease 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 logSecurity 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.
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 logA 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.