ProvenPin Install

FastAPI endpoints rate limited with slowapi, tested with TestClient: working versions (tested 2026-10-15)

Add per-route and per-API-key rate limits to FastAPI with slowapi, then test 429 behavior offline with TestClient and the in-memory store.

passed5 passed · 0 failed2026-10-15 13:29 UTC
Status
passed
Last passed
2026-10-15 13:29 UTC
Tests
5 / 5
Language
Python
Runtime
Python 3.13.16
pinned versions4
fastapi@0.143.1slowapi@0.1.10httpx@0.28.1pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
Python 3.13.16
linux 6.17.0-1022-azure
x64
runner: github-actions 38742087302

The code

Exactly the files that passed. Your agent can pull them with get_recipe("fastapi-slowapi-rate-limit") (install), or see them on GitHub.

requirements.txt
fastapi==0.143.1
slowapi==0.1.10
httpx==0.28.1
pytest==9.1.1
app.py
from fastapi import FastAPI, Request
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from slowapi.util import get_remote_address

def api_key_or_ip(request: Request) -> str:
    return request.headers.get("X-API-Key") or get_remote_address(request)

limiter = Limiter(key_func=get_remote_address, storage_uri="memory://")

app = FastAPI()
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

@app.get("/ping")
@limiter.limit("3/minute")
def ping(request: Request):
    # slowapi needs the Request in the signature to read the client address
    return {"ok": True}

@app.get("/search")
@limiter.limit("2/minute", key_func=api_key_or_ip)
def search(request: Request, q: str = ""):
    return {"q": q}
tests/test_app.py
import pytest
from fastapi.testclient import TestClient

from app import app, limiter

@pytest.fixture(autouse=True)
def fresh_limits():
    # the memory store is process-wide, so clear it before and after every test
    limiter.reset()
    limiter.enabled = True
    yield
    limiter.reset()
    limiter.enabled = True

@pytest.fixture
def client():
    with TestClient(app) as c:
        yield c

def test_allows_up_to_limit(client):
    assert [client.get("/ping").status_code for _ in range(3)] == [200, 200, 200]

def test_returns_429_over_limit(client):
    for _ in range(3):
        client.get("/ping")
    r = client.get("/ping")
    assert r.status_code == 429
    assert "Rate limit exceeded" in r.text

def test_keys_have_separate_buckets(client):
    # TestClient always comes from the same IP, so only the API key differs here
    for _ in range(2):
        assert client.get("/search", params={"q": "a"}, headers={"X-API-Key": "k1"}).status_code == 200
    assert client.get("/search", params={"q": "a"}, headers={"X-API-Key": "k1"}).status_code == 429
    assert client.get("/search", params={"q": "b"}, headers={"X-API-Key": "k2"}).status_code == 200

def test_reset_restores_budget(client):
    for _ in range(3):
        client.get("/ping")
    assert client.get("/ping").status_code == 429
    limiter.reset()
    assert client.get("/ping").status_code == 200

def test_disabled_limiter_bypasses_limits(client):
    limiter.enabled = False
    assert all(client.get("/ping").status_code == 200 for _ in range(10))
pytest.ini
[pytest]
pythonpath = .

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.