Configuration¶
Choosing a backend¶
DatabaseBackend |
RedisBackend |
|
|---|---|---|
| Setup | Just migrate |
Requires a Redis server + [redis] extra |
| TTL / expiry | Manual (cleanup_expired_idempotency_keys) |
Automatic (native Redis EX) |
| Works without extra infrastructure | Yes | No — needs Redis |
| Performance under high volume | Good; bounded by your DB | Excellent — purpose-built for this |
| Survives a database failover/restart independent of your primary DB | No (shares your DB) | Yes (if Redis is separate) |
Start with DatabaseBackend if you don't already run Redis — it needs
nothing beyond a migration. Move to RedisBackend if idempotency records
become a meaningful fraction of your database's write volume, or if you
already run Redis for caching/sessions and want to keep this off your
primary database.
IDEMPOTENCY = {
"BACKEND": "drf_idempotency.backends.database.DatabaseBackend",
}
# or
IDEMPOTENCY = {
"BACKEND": "drf_idempotency.backends.redis.RedisBackend",
"BACKEND_OPTIONS": {"url": "redis://localhost:6379/2"},
}
Choosing which methods are covered¶
Defaults to ["POST", "PUT", "PATCH"] — the methods that create or
mutate state. GET/HEAD/OPTIONS/DELETE are excluded by default
since GET is naturally idempotent and DELETE's idempotency semantics
(deleting an already-deleted resource) are usually handled fine by
returning 404 on the second attempt. Add DELETE explicitly if your
delete endpoints have side effects (e.g. triggering a downstream webhook)
that shouldn't repeat:
IDEMPOTENCY = {"METHODS": ["POST", "PUT", "PATCH", "DELETE"]}
Deciding on REQUIRE_KEY¶
Leave disabled (default) while rolling this out — requests without a key simply skip idempotency handling, so nothing breaks for clients that haven't adopted the header yet. Enable once every client sends a key, to close the gap entirely:
IDEMPOTENCY = {"REQUIRE_KEY": True}
Tuning TTLs¶
TTL_SECONDS(default 24h): how long a completed response stays replayable. Match this to how long a client might reasonably retry (Stripe uses 24 hours).LOCK_TTL_SECONDS(default 30s): how long an in-progress lock is held before being considered abandoned. Set this comfortably above your view's worst-case (but still successful) execution time — too short, and a legitimately slow request's lock could be reclaimed by a retry while the original is still running; too long, and a genuinely crashed worker blocks retries longer than necessary.
Deciding on CACHE_CLIENT_ERROR_RESPONSES¶
Enabled by default, matching Stripe: a 4xx validation error is
deterministic for a given request body, so caching and replaying it is
safe and saves the client from re-triggering the same validation logic.
Disable only if your view's error responses have side effects that
shouldn't be silently skipped on retry (unusual, but possible with some
custom exception handling).
Example project-wide configuration¶
IDEMPOTENCY = {
"BACKEND": "drf_idempotency.backends.redis.RedisBackend",
"BACKEND_OPTIONS": {"url": "redis://localhost:6379/2"},
"TTL_SECONDS": 86400,
"LOCK_TTL_SECONDS": 45,
"REQUIRE_KEY": True,
"METHODS": ["POST", "PUT", "PATCH", "DELETE"],
}