aiolimiter AsyncLimiter guarding httpx AsyncClient offline: working versions (tested 2026-10-16)
Throttle an httpx AsyncClient with aiolimiter's leaky-bucket AsyncLimiter and test the burst, serialized concurrency, error and event-loop reuse gotchas fully offline with httpx.MockTransport.
passed6 passed · 0 failed2026-10-16 00:01 UTC- Status
- passed
- Last passed
- 2026-10-16 00:01 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38826982604
The code
Exactly the files that passed. Your agent can pull them with get_recipe("aiolimiter-httpx-rate-limit") (install), or see them on GitHub.
aiolimiter==1.3.0 httpx==0.28.1 pytest-asyncio==1.4.0 pytest==9.1.1
import httpx
from aiolimiter import AsyncLimiter
class RateLimitedClient:
def __init__(
self,
max_rate: float = 5,
time_period: float = 1.0,
base_url: str = "https://api.example.com",
transport: httpx.AsyncBaseTransport | None = None,
):
self.limiter = AsyncLimiter(max_rate, time_period)
self.http = httpx.AsyncClient(base_url=base_url, timeout=10, transport=transport)
async def get_json(self, path: str) -> dict:
async with self.limiter:
r = await self.http.get(path)
r.raise_for_status()
return r.json()
async def aclose(self) -> None:
await self.http.aclose()
import asyncio
import time
import httpx
import pytest
from aiolimiter import AsyncLimiter
from client import RateLimitedClient
def make_client(handler, max_rate=10, time_period=1.0):
return RateLimitedClient(max_rate, time_period, transport=httpx.MockTransport(handler))
async def test_parses_json_offline():
seen = []
def handler(request):
seen.append(request.url.path)
return httpx.Response(200, json={"path": request.url.path})
c = make_client(handler)
assert await c.get_json("/repos/a/b") == {"path": "/repos/a/b"}
assert seen == ["/repos/a/b"]
await c.aclose()
async def test_http_error_propagates():
c = make_client(lambda request: httpx.Response(500))
with pytest.raises(httpx.HTTPStatusError):
await c.get_json("/boom")
await c.aclose()
async def test_initial_burst_is_free():
c = make_client(lambda request: httpx.Response(200, json={}), max_rate=3, time_period=1.0)
start = time.monotonic()
for _ in range(3):
await c.get_json("/x")
assert time.monotonic() - start < 0.2
await c.aclose()
async def test_gather_is_throttled():
c = make_client(lambda request: httpx.Response(200, json={}), max_rate=1, time_period=0.2)
start = time.monotonic()
await asyncio.gather(*(c.get_json(f"/x{i}") for i in range(3)))
assert time.monotonic() - start >= 0.35
await c.aclose()
async def test_acquire_amount_over_capacity_raises():
limiter = AsyncLimiter(max_rate=2, time_period=1)
with pytest.raises(ValueError):
await limiter.acquire(amount=3)
def test_limiter_reuse_across_event_loops_warns():
limiter = AsyncLimiter(max_rate=1, time_period=1)
async def use():
async with limiter:
pass
asyncio.run(use())
with pytest.warns(RuntimeWarning, match="re-used across loops"):
asyncio.run(use())
[pytest] pythonpath = . asyncio_mode = auto
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
authlib OAuth2 client credentials flow + httpx mocked by respx
An async authlib OAuth2 client that fetches, caches and auto-refreshes client_credentials tokens over httpx, tested fully offline with respx route mocks.
FastAPI config via pydantic-settings as a cached dependency
Load config from env vars with pydantic-settings, inject it into FastAPI routes through a cached dependency, and test it offline with monkeypatch and dependency_overrides.
FastAPI dependency_overrides tested offline with httpx ASGITransport
Override FastAPI dependencies (including nested ones) and drive the app in-process with httpx.ASGITransport, no network and no running server.
FastAPI endpoint with Pydantic v2 discriminated unions and Annotated validators
Accept a tagged union of payment methods with Field(discriminator), add Annotated validators, and test the exact 422 error shapes offline with TestClient.