Idempotency-Key middleware for FastAPI, with pluggable Redis / in-memory storage backends.
Project description
mobius-idempotence-library
Idempotency-Key middleware for FastAPI, with pluggable storage backends.
Protects POST/PATCH-style endpoints (payments, order creation, anything
non-idempotent by nature) against duplicate side effects when a client
retries a request -- due to a timeout, a dropped connection, or an
at-least-once delivery mechanism upstream.
Features
- Idempotency-Key enforcement following the shape of the common
Idempotency-Keyheader convention used by Stripe and similar APIs. - Request fingerprinting: a key reused with a different request body is
rejected (
422), not silently replayed. - In-flight collision detection: a second concurrent request with the
same key while the first is still processing gets
409, not a race. - Pluggable backends: ships with
RedisBackend(distributed, atomic via single-round-trip Lua scripts) andMemoryBackend(in-process, for tests or single-instance deployments). ImplementStorageBackendfor anything else (Postgres, DynamoDB, ...). - Fail-open or fail-closed, your choice, when the backend is unreachable.
- Fully async, doesn't block the event loop.
Install
pip install mobius-idempotence-library # in-memory backend only
pip install mobius-idempotence-library[redis] # + Redis backend
Quick start
from fastapi import FastAPI
from pydantic import BaseModel
from mobius_idempotence import IdempotencyConfig, IdempotencyManager, idempotent
from mobius_idempotence.backends.redis import RedisBackend
app = FastAPI()
backend = RedisBackend("redis://localhost:6379/0")
manager = IdempotencyManager(backend, IdempotencyConfig(fail_open=False))
class PaymentRequest(BaseModel):
amount: float
currency: str
@app.post("/payments", status_code=201)
@idempotent(manager, status_code=201)
async def create_payment(payload: PaymentRequest):
return await charge_the_card(payload) # your business logic, nothing else
@app.on_event("shutdown")
async def shutdown():
await manager.aclose()
That's the entire integration. @idempotent(manager) handles:
- validating the
Idempotency-Keyheader - fingerprinting the request body, so a key reused with a different body is rejected (
422) - detecting an in-flight duplicate request (
409) - replaying the cached response verbatim on retry, with no re-run of your handler
- releasing the lock if your handler raises, so the client can safely retry with the same key
No Depends(...), no manual cache lookup, no manual save/release calls in the handler.
Callers must send the same Idempotency-Key header (and the same request
body) on retry:
curl -X POST http://localhost:8000/payments \
-H "Idempotency-Key: 3f29c1a2-..." \
-H "Content-Type: application/json" \
-d '{"amount": 10.5, "currency": "USD"}'
What gets cached: the handler's return value (dict, list, Pydantic
model, or dataclass), encoded the same way FastAPI itself would encode it.
If you need to return a raw Response / StreamingResponse, or want custom
caching logic, use the lower-level IdempotencyGuard dependency instead --
see examples/payments_app_manual.py.
Writing your own backend
Subclass mobius_idempotence.StorageBackend and implement five async
methods: check_or_lock, save_response, release, ping, aclose. See
src/mobius_idempotence/backends/memory.py for the simplest possible
reference implementation.
Configuration
IdempotencyConfig is a plain dataclass -- wire it up to whatever config
system your app already uses (env vars, Pydantic Settings, a YAML file):
| Field | Default | Meaning |
|---|---|---|
lock_ttl |
60 |
Seconds an in-progress lock lives before being considered abandoned |
response_ttl |
86400 |
Seconds a finished response stays cached and replayable |
key_prefix |
"idemp:" |
Storage key namespace |
fail_open |
True |
If the backend is unreachable: process anyway (True) or 503 (False) |
max_key_len |
255 |
Reject longer Idempotency-Key headers |
Testing
pip install -e ".[dev]"
pytest
License
MIT
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 Distribution
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 mobius_idempotence_library-0.1.1.tar.gz.
File metadata
- Download URL: mobius_idempotence_library-0.1.1.tar.gz
- Upload date:
- Size: 16.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
66950aa8ac998d6b183f72be9edf2f813bfca1ca9db93cfe0d474a6e83397b78
|
|
| MD5 |
cd21a8374804b0715fb84d94eb6edc61
|
|
| BLAKE2b-256 |
95ae8cf7034d8641f4be1a81a43a8293d2bb8f8b5e45ebe73592e094eeca66a2
|
File details
Details for the file mobius_idempotence_library-0.1.1-py3-none-any.whl.
File metadata
- Download URL: mobius_idempotence_library-0.1.1-py3-none-any.whl
- Upload date:
- Size: 19.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8d4f93f41f0e66e5192d17a5ec65005b7465207352a7c829a349d62b5ad14b06
|
|
| MD5 |
f76e285e96e3f5d1bab9c158401827ca
|
|
| BLAKE2b-256 |
6dd35a4726cba841b57467fc892d96c302ce5b4e967df72234e039e99be3ea7d
|