{"id":51544994,"url":"https://github.com/robertolima-dev/rust-py-rate-limit","last_synced_at":"2026-07-09T17:01:56.661Z","repository":{"id":366841873,"uuid":"1278099725","full_name":"robertolima-dev/rust-py-rate-limit","owner":"robertolima-dev","description":null,"archived":false,"fork":false,"pushed_at":"2026-06-23T14:02:34.000Z","size":38,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-23T15:27:09.954Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/robertolima-dev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-23T13:12:51.000Z","updated_at":"2026-06-23T14:03:52.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/robertolima-dev/rust-py-rate-limit","commit_stats":null,"previous_names":["robertolima-dev/rust-py-rate-limit"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/robertolima-dev/rust-py-rate-limit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/robertolima-dev%2Frust-py-rate-limit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/robertolima-dev%2Frust-py-rate-limit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/robertolima-dev%2Frust-py-rate-limit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/robertolima-dev%2Frust-py-rate-limit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/robertolima-dev","download_url":"https://codeload.github.com/robertolima-dev/rust-py-rate-limit/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/robertolima-dev%2Frust-py-rate-limit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35306717,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-09T02:00:07.329Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-09T17:01:54.994Z","updated_at":"2026-07-09T17:01:56.655Z","avatar_url":"https://github.com/robertolima-dev.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# rust-py-rate-limit\n\n\u003e **Fast local rate limiting for Python, powered by Rust.**\n\n🌐 **Website:** [rust-py-rate-limit.vercel.app](https://rust-py-rate-limit.vercel.app/)\n\nA fast, thread-safe, in-process rate limiter for Python with a core written in\n**Rust** (via [PyO3](https://pyo3.rs) + [maturin](https://www.maturin.rs)). Use\nit to protect endpoints, functions, internal APIs, workers and backend scripts\nagainst bursts of traffic — with zero external services.\n\n```python\nfrom rust_py_rate_limit import RateLimiter\n\nlimiter = RateLimiter(limit=10, window_seconds=60)\n\nif limiter.allow(\"user:123\"):\n    print(\"allowed\")\nelse:\n    print(\"blocked\")\n```\n\n---\n\n## Table of contents\n\n- [What is this?](#what-is-this)\n- [Why Rust?](#why-rust)\n- [Installation](#installation)\n- [Quick start](#quick-start)\n- [How Fixed Window works](#how-fixed-window-works)\n- [How Sliding Window works](#how-sliding-window-works)\n- [API reference](#api-reference)\n- [FastAPI](#fastapi)\n- [Django](#django)\n- [Flask](#flask)\n- [Decorator](#decorator)\n- [Statistics](#statistics)\n- [Limitations](#limitations)\n- [Roadmap](#roadmap)\n- [Development](#development)\n- [License](#license)\n\n---\n\n## What is this?\n\n`rust-py-rate-limit` is a **local** (in-process) rate limiter. Every limiter\ninstance keeps its counters in memory inside your Python process, guarded by a\nconcurrent, sharded hash map on the Rust side. There is no Redis, no network\nhop, and no serialization on the hot path — just a couple of atomic operations\nper request.\n\nIt works anywhere Python runs:\n\n- Plain Python\n- FastAPI\n- Django\n- Flask (preview)\n- Background workers and scripts\n\n## Why Rust?\n\n- **Speed** — the counting logic is compiled native code; the hot path releases\n  the GIL so multiple Python threads can check limits in parallel.\n- **Safety** — no data races by construction. State lives in a\n  [`DashMap`](https://docs.rs/dashmap) (a sharded concurrent map) and statistics\n  use lock-free atomics, so there is no global lock on the critical path.\n- **Simplicity** — a tiny, predictable API surface that is hard to misuse.\n\n## Installation\n\n```bash\npip install rust-py-rate-limit\n```\n\nRequires Python 3.10+. Wheels are published for Linux, macOS and Windows, so no\nRust toolchain is needed to install.\n\n## Quick start\n\n```python\nfrom rust_py_rate_limit import RateLimiter\n\nlimiter = RateLimiter(limit=3, window_seconds=60)\n\nassert limiter.allow(\"ip:127.0.0.1\") is True\nassert limiter.allow(\"ip:127.0.0.1\") is True\nassert limiter.allow(\"ip:127.0.0.1\") is True\nassert limiter.allow(\"ip:127.0.0.1\") is False   # limit reached\n```\n\nOpt into the **Sliding Window** algorithm to smooth bursts at the window\nboundary (the default is `\"fixed\"`):\n\n```python\nlimiter = RateLimiter(limit=100, window_seconds=60, algorithm=\"sliding\")\nlimiter.algorithm  # \"sliding\"\n```\n\n## How Fixed Window works\n\nThe default algorithm is **Fixed Window**. Each key gets a counter and a window\nstart time. Within a window of `window_seconds`, up to `limit` requests are\nadmitted; once the window elapses, the counter resets.\n\n```text\nlimit = 3, window = 60s, key = \"user:1\"\n\nrequest 1 -\u003e allowed\nrequest 2 -\u003e allowed\nrequest 3 -\u003e allowed\nrequest 4 -\u003e blocked\n... 60s later ...\nrequest 5 -\u003e allowed   (new window)\n```\n\nFixed Window is simple and cheap. Its only caveat is that it can admit up to\n`2 * limit` requests around a window boundary (a burst at the end of one window\nplus a burst at the start of the next). If you need stricter smoothing, use\n`algorithm=\"sliding\"` (below).\n\n## How Sliding Window works\n\nSet `algorithm=\"sliding\"` for the **sliding window counter**, which removes the\nboundary doubling. It keeps the count for the current aligned window plus the\nprevious one, and weights the previous window by how much of it still overlaps\nthe trailing `window_seconds` ending at *now*:\n\n```text\nestimated = previous_count * weight + current_count\nweight    = (window_seconds - elapsed_in_current_window) / window_seconds\n```\n\nA request is admitted while `estimated \u003c limit`. This is O(1) in time and memory\nper key (unlike a sliding *log*, which stores every timestamp) and smooths the\nburst at the boundary, at the cost of being an approximation rather than an exact\ncount.\n\n```python\nlimiter = RateLimiter(limit=10, window_seconds=60, algorithm=\"sliding\")\n# A full burst at the end of one window leaves far fewer slots right after the\n# boundary, instead of a fresh `limit` as Fixed Window would.\n```\n\n## API reference\n\n```python\nRateLimiter(limit: int, window_seconds: int, algorithm: str = \"fixed\")\n```\n\n`limit` and `window_seconds` must be **positive integers** (passing `0` or a\nnegative value raises `ValueError`). `algorithm` selects the strategy —\n`\"fixed\"` (default) or `\"sliding\"`; any other value raises `ValueError`.\n\n| Method | Returns | Description |\n| --- | --- | --- |\n| `allow(key: str)` | `bool` | Consume one request. `True` if admitted, `False` if blocked. |\n| `check(key: str)` | `dict` | Consume one request and return full detail (see below). |\n| `remaining(key: str)` | `int` | Requests left in the current window **without** consuming one. |\n| `reset(key: str)` | `bool` | Drop a key's state. `True` if it existed. |\n| `clear()` | `None` | Drop all keys. |\n| `stats()` | `dict` | Activity counters (see [Statistics](#statistics)). |\n| `cleanup_expired()` | `int` | Remove keys whose window has expired. Returns the count removed. |\n\nRead-only properties: `limiter.max_requests`, `limiter.window_seconds` and\n`limiter.algorithm` (`\"fixed\"` or `\"sliding\"`). (The configured limit is\n`max_requests`, since `.limit(...)` is the decorator.)\n\n### `check()` return value\n\nAllowed:\n\n```python\n{\n    \"allowed\": True,\n    \"limit\": 100,\n    \"remaining\": 99,\n    \"reset_after_seconds\": 60,\n    \"retry_after_seconds\": 0,\n}\n```\n\nBlocked:\n\n```python\n{\n    \"allowed\": False,\n    \"limit\": 100,\n    \"remaining\": 0,\n    \"reset_after_seconds\": 42,\n    \"retry_after_seconds\": 42,\n}\n```\n\n## FastAPI\n\n### Manual check\n\n```python\nfrom fastapi import FastAPI, Request, HTTPException\nfrom rust_py_rate_limit import RateLimiter\n\napp = FastAPI()\nlimiter = RateLimiter(limit=100, window_seconds=60)\n\n@app.get(\"/api/users\")\ndef list_users(request: Request):\n    key = request.client.host\n    if not limiter.allow(key):\n        raise HTTPException(status_code=429, detail=\"Too many requests\")\n    return {\"users\": []}\n```\n\n### Middleware\n\n```python\nfrom rust_py_rate_limit.fastapi import RateLimitMiddleware\n\napp.add_middleware(\n    RateLimitMiddleware,\n    limit=100,\n    window_seconds=60,\n    key_func=lambda request: request.client.host,\n)\n```\n\nWhen a request is blocked the middleware responds with `429` and\n`{\"detail\": \"Too many requests\"}`. Every response carries the standard headers:\n\n```text\nX-RateLimit-Limit\nX-RateLimit-Remaining\nX-RateLimit-Reset\nRetry-After      (only when blocked)\n```\n\n## Django\n\n```python\n# settings.py\nMIDDLEWARE = [\n    # ...\n    \"rust_py_rate_limit.django.RateLimitMiddleware\",\n]\n\nRUST_PY_RATE_LIMIT = {\n    \"LIMIT\": 100,\n    \"WINDOW_SECONDS\": 60,\n    \"KEY\": \"ip\",  # \"ip\" or \"user\"\n}\n```\n\nOr check manually in a view:\n\n```python\nfrom django.http import JsonResponse\nfrom rust_py_rate_limit import RateLimiter\n\nlimiter = RateLimiter(limit=100, window_seconds=60)\n\ndef my_view(request):\n    key = request.META.get(\"REMOTE_ADDR\")\n    if not limiter.allow(key):\n        return JsonResponse({\"detail\": \"Too many requests\"}, status=429)\n    return JsonResponse({\"ok\": True})\n```\n\n## Flask\n\n```python\nfrom flask import Flask\nfrom rust_py_rate_limit.flask import FlaskRateLimiter\n\napp = Flask(__name__)\nlimiter = FlaskRateLimiter(app, limit=100, window_seconds=60)\n\n@app.get(\"/api/users\")\n@limiter.limit()\ndef list_users():\n    return {\"users\": []}\n```\n\n## Decorator\n\n```python\nfrom rust_py_rate_limit import RateLimiter, RateLimitExceeded\n\nlimiter = RateLimiter(limit=5, window_seconds=60)\n\n@limiter.limit(\"login\")\ndef login():\n    return \"ok\"\n```\n\nWhen the limit is exceeded the decorated function raises `RateLimitExceeded`\n(which carries `.key`, `.limit` and `.retry_after`). The key may also be a\ncallable that derives the key from the function's arguments:\n\n```python\n@limiter.limit(lambda user_id: f\"user:{user_id}\")\ndef fetch(user_id):\n    ...\n```\n\n## Statistics\n\n```python\nlimiter.stats()\n# {\n#     \"allowed\": 1200,\n#     \"blocked\": 35,\n#     \"total_checks\": 1235,\n#     \"active_keys\": 20,\n# }\n```\n\n## Limitations\n\nBe honest with yourself about what an in-process limiter can and cannot do:\n\n- The rate-limit state is **local to the process**.\n- Under Gunicorn/Uvicorn with **multiple workers**, each worker keeps its own\n  counters, so the effective global limit is roughly `limit × workers`.\n- It is **not** a replacement for Redis when you need distributed rate limiting.\n- Fixed Window can allow short bursts at the boundary between two windows; use\n  `algorithm=\"sliding\"` to smooth them.\n- For distributed production setups, a Redis/Postgres backend is planned (see\n  the roadmap).\n\n## Roadmap\n\n| Version | Highlights | Status |\n| --- | --- | --- |\n| **v0.1.0** | Fixed Window · `allow`/`check`/`remaining`/`reset`/`clear`/`stats`/`cleanup_expired` · pytest · README | ✅ |\n| **v0.1.5** | Decorator · FastAPI/Django/Flask middleware · HTTP headers | ✅ |\n| **v0.2.0** | Sliding Window (`algorithm=\"sliding\"`) | ✅ |\n| v0.3.0 | Token Bucket · background cleanup | 🔜 |\n| v0.4.0 | Redis backend · distributed rate limiting | 🔜 |\n| v0.5.0 | Prometheus metrics · ImmutableLog integration | 🔜 |\n\n## Development\n\n```bash\n# Rust unit tests\ncargo test\n\n# Build the extension into a virtualenv and run the Python tests\npython -m venv .venv \u0026\u0026 source .venv/bin/activate\npip install -e \".[dev]\"      # or: pip install maturin \u0026\u0026 maturin develop\nmaturin develop\npytest\n```\n\n## License\n\n[MIT](LICENSE) © Roberto Lima\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frobertolima-dev%2Frust-py-rate-limit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frobertolima-dev%2Frust-py-rate-limit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frobertolima-dev%2Frust-py-rate-limit/lists"}