Fast local rate limiting for Python, powered by Rust.
Project description
rust-py-rate-limit
Fast local rate limiting for Python, powered by Rust.
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
- API reference
- FastAPI
- Django
- Flask
- Decorator
- 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
How Fixed Window works
The MVP implements the Fixed Window algorithm. 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, the
roadmap includes Sliding Window and Token Bucket.
API reference
RateLimiter(limit: int, window_seconds: int)
Both arguments must be positive integers. Passing 0 (or a negative 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 and limiter.window_seconds.
(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.
- For distributed production setups, a Redis/Postgres backend is planned (see the roadmap).
Roadmap
| Version | Highlights |
|---|---|
| v0.1.0 | Fixed Window · allow/check/remaining/reset/clear/stats/cleanup_expired · pytest · README |
| v0.2.0 | Decorator · FastAPI/Django middleware · HTTP headers |
| v0.3.0 | Sliding Window · 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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rust_py_rate_limit-0.1.1.tar.gz.
File metadata
- Download URL: rust_py_rate_limit-0.1.1.tar.gz
- Upload date:
- Size: 27.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5e1df5f201263e04a079c5c8054c926be7174a3090c840a8e8c8c54d91e7dfd6
|
|
| MD5 |
d3fd3a8e4eb7abf1677c1756fa498e86
|
|
| BLAKE2b-256 |
9f4b7d557ab44f05afd56c1e8cedabe001fd6addcb5d9a2086d2fe21702dc653
|
Provenance
The following attestation bundles were made for rust_py_rate_limit-0.1.1.tar.gz:
Publisher:
release.yml on robertolima-dev/rust-py-rate-limit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
rust_py_rate_limit-0.1.1.tar.gz -
Subject digest:
5e1df5f201263e04a079c5c8054c926be7174a3090c840a8e8c8c54d91e7dfd6 - Sigstore transparency entry: 1927154988
- Sigstore integration time:
-
Permalink:
robertolima-dev/rust-py-rate-limit@f04d10eac263cb368b77aea30a11fcc66d7c4dd1 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/robertolima-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f04d10eac263cb368b77aea30a11fcc66d7c4dd1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file rust_py_rate_limit-0.1.1-cp312-cp312-win_amd64.whl.
File metadata
- Download URL: rust_py_rate_limit-0.1.1-cp312-cp312-win_amd64.whl
- Upload date:
- Size: 151.0 kB
- Tags: CPython 3.12, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3a9dca37f8b85a82eec7782e26ee366b2b4b7b84b55a34c0924147866eea9243
|
|
| MD5 |
cdd695e2078a55f575ddcffce7f2323d
|
|
| BLAKE2b-256 |
7fc4a7c05be77224cc138dbeba84a5a107e1a4bf3907188b80a3c4690e5dc98b
|
Provenance
The following attestation bundles were made for rust_py_rate_limit-0.1.1-cp312-cp312-win_amd64.whl:
Publisher:
release.yml on robertolima-dev/rust-py-rate-limit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
rust_py_rate_limit-0.1.1-cp312-cp312-win_amd64.whl -
Subject digest:
3a9dca37f8b85a82eec7782e26ee366b2b4b7b84b55a34c0924147866eea9243 - Sigstore transparency entry: 1927155941
- Sigstore integration time:
-
Permalink:
robertolima-dev/rust-py-rate-limit@f04d10eac263cb368b77aea30a11fcc66d7c4dd1 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/robertolima-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f04d10eac263cb368b77aea30a11fcc66d7c4dd1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file rust_py_rate_limit-0.1.1-cp312-cp312-manylinux_2_34_x86_64.whl.
File metadata
- Download URL: rust_py_rate_limit-0.1.1-cp312-cp312-manylinux_2_34_x86_64.whl
- Upload date:
- Size: 272.3 kB
- Tags: CPython 3.12, manylinux: glibc 2.34+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cf31980817101186c462ca998431b9d42a220e923f9c383f2f435600f5a23f69
|
|
| MD5 |
c5548c29e5fdf674b9d005a5bf83948c
|
|
| BLAKE2b-256 |
073f76c73fccd198235c195ee06d4838a6a99747c7d22d444f9e46be1b2c8e1c
|
Provenance
The following attestation bundles were made for rust_py_rate_limit-0.1.1-cp312-cp312-manylinux_2_34_x86_64.whl:
Publisher:
release.yml on robertolima-dev/rust-py-rate-limit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
rust_py_rate_limit-0.1.1-cp312-cp312-manylinux_2_34_x86_64.whl -
Subject digest:
cf31980817101186c462ca998431b9d42a220e923f9c383f2f435600f5a23f69 - Sigstore transparency entry: 1927157606
- Sigstore integration time:
-
Permalink:
robertolima-dev/rust-py-rate-limit@f04d10eac263cb368b77aea30a11fcc66d7c4dd1 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/robertolima-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f04d10eac263cb368b77aea30a11fcc66d7c4dd1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file rust_py_rate_limit-0.1.1-cp312-cp312-macosx_11_0_arm64.whl.
File metadata
- Download URL: rust_py_rate_limit-0.1.1-cp312-cp312-macosx_11_0_arm64.whl
- Upload date:
- Size: 244.1 kB
- Tags: CPython 3.12, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
945550782f68fdc9acf5299e7c53a5c9807ceea4305d0a41580768a4a7df868c
|
|
| MD5 |
3bc6bf10d7e255ab4fd9a7e5d7a1f6f8
|
|
| BLAKE2b-256 |
0912cf057bab8cc271242dd5b050ff33fbdf36b3c8ff075c16ae6cb0f456bf60
|
Provenance
The following attestation bundles were made for rust_py_rate_limit-0.1.1-cp312-cp312-macosx_11_0_arm64.whl:
Publisher:
release.yml on robertolima-dev/rust-py-rate-limit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
rust_py_rate_limit-0.1.1-cp312-cp312-macosx_11_0_arm64.whl -
Subject digest:
945550782f68fdc9acf5299e7c53a5c9807ceea4305d0a41580768a4a7df868c - Sigstore transparency entry: 1927157028
- Sigstore integration time:
-
Permalink:
robertolima-dev/rust-py-rate-limit@f04d10eac263cb368b77aea30a11fcc66d7c4dd1 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/robertolima-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f04d10eac263cb368b77aea30a11fcc66d7c4dd1 -
Trigger Event:
push
-
Statement type: