Advanced Usage¶
Using the Redis backend¶
pip install drf-idempotency[redis]
IDEMPOTENCY = {
"BACKEND": "drf_idempotency.backends.redis.RedisBackend",
"BACKEND_OPTIONS": {"url": "redis://localhost:6379/2", "key_prefix": "myapp-idempotency:"},
}
Or reuse an existing client instance (e.g. one you already configured with connection pooling elsewhere):
import redis
_client = redis.Redis(host="localhost", port=6379, db=2)
IDEMPOTENCY = {
"BACKEND": "drf_idempotency.backends.redis.RedisBackend",
"BACKEND_OPTIONS": {"client": _client},
}
Using the database backend¶
IDEMPOTENCY = {"BACKEND": "drf_idempotency.backends.database.DatabaseBackend"}
python manage.py migrate
python manage.py cleanup_expired_idempotency_keys # run periodically
Works identically on PostgreSQL, MySQL, and SQLite — atomicity comes from
a unique constraint plus Django's own get_or_create (which catches the
resulting IntegrityError and re-fetches), not any database-specific
locking feature.
Writing a custom backend¶
Implement BaseBackend's five methods:
from datetime import timedelta
from collections.abc import Mapping
from drf_idempotency.backends.base import AcquireResult, BaseBackend, StoredRecord
class MemcachedBackend(BaseBackend):
def acquire_or_get(self, key: str, fingerprint: str, *, lock_ttl: timedelta) -> AcquireResult:
...
def complete(self, key: str, *, status_code: int, headers: Mapping[str, str], body: bytes, ttl: timedelta) -> None:
...
def fail(self, key: str) -> None:
...
def get(self, key: str) -> StoredRecord | None:
...
def cleanup_expired(self) -> int:
...
The correctness of the whole package hinges on acquire_or_get being
genuinely atomic: if two callers invoke it concurrently for the same key,
exactly one must get acquired=True. See
drf_idempotency.backends.redis.RedisBackend (built on Redis's atomic
SET NX) and drf_idempotency.backends.database.DatabaseBackend (built
on a unique constraint) for two different ways to achieve this — pick
whichever your storage technology naturally supports.
IDEMPOTENCY = {"BACKEND": "myapp.backends.MemcachedBackend"}
Combining the decorator with global middleware¶
If IdempotencyMiddleware is installed project-wide and a specific
view also carries @idempotent() (perhaps left over from before the
middleware was added), both wrapping the same request is handled safely:
the outer layer (the middleware) performs the real acquire-or-replay
logic, and the inner layer (the decorator) detects this via a marker
already set on the request and simply calls the view directly — no
spurious 409 Conflict against your own outer layer. See
Architecture for why this is
necessary and how it works. Even so, prefer using one integration
point per view for clarity — mixing them project-wide is more likely to
confuse a future reader than to serve a real purpose.
Combining with other middleware¶
IdempotencyMiddleware reads request.body (via fingerprinting) before
your view runs. This is compatible with Django's standard middleware
ordering — place it wherever you'd place any other middleware that needs
to inspect the request body (typically after security/session middleware,
before your own custom business-logic middleware).
Inspecting a stored record directly¶
For debugging or admin tooling:
from drf_idempotency.core import get_backend
record = get_backend().get("some-key")
if record is not None:
print(record.status, record.response_status_code)
Multiple backends in one project¶
Nothing prevents constructing a second backend instance directly (bypassing
the global IDEMPOTENCY setting) for a specific use case — e.g. a
short-TTL, in-memory-like Redis instance for one particularly
high-volume, latency-sensitive endpoint, alongside the database backend
for everything else. Call RedisBackend(...)/DatabaseBackend(...)
directly and use drf_idempotency.core.process_idempotent_request
yourself rather than the standard middleware/decorator, if you need this
level of control.