Measured on 0.2.0 with the Redis repository. Two identical calls, same operation and key, 50 ms apart, with an action that takes 300 ms (a payment provider round trip):
--- two identical requests, 50 ms apart, same key ---
responses: ['ch_1', 'ch_1']
charges made by the provider: 2 -> ['ch_1', 'ch_2']
Both callers got the same response, and the card was charged twice. Rule 2 on the agents page says exactly this will happen, and the "WRONG — treating the decorator as a mutex" example shows it, so this is a known consequence of the design: the check is a GET, the write is a SET NX after the action, and nothing marks the key as taken in between. But the case that matters most for an idempotency key is a client that timed out and retried while the original is still in flight, and that is precisely the window where the library runs the action twice. The architecture page also still says the pattern "ensures at-most-once or exactly-once semantics", which the measurement contradicts.
What I think it needs is an in-flight reservation: before running the action, SET NX a pending marker under the same key with a lease (say 30 s, configurable), then run, then replace the marker with the final record. A second caller that finds a pending marker either waits for the final record (poll with a bound, the lease) or fails fast with a new IdempotencyInProgressError that an HTTP layer maps to 409, which is what Stripe does. An action that raises deletes the marker so the retry runs again, consistent with "failures are not cached". A marker whose lease expired is treated as absent, so a crashed worker cannot wedge a key. The mode belongs on the coordinator (in_flight="wait" | "raise", plus the lease); I lean towards reservation on by default in the next minor, with the old both-run behaviour available as in_flight="run" for callers whose action is genuinely safe to repeat, because a default that double-charges is the wrong default even when documented. Rule 2, the mutex example and the architecture page follow the code.
Script: measure.py in https://github.com/bedrock-python/bedrock-python.github.io/tree/docs/production-python-series/docs/blog/lab/2026-09-07-idempotency-keys. Related: the fingerprint issue filed alongside this one.
Measured on 0.2.0 with the Redis repository. Two identical calls, same operation and key, 50 ms apart, with an action that takes 300 ms (a payment provider round trip):
Both callers got the same response, and the card was charged twice. Rule 2 on the agents page says exactly this will happen, and the "WRONG — treating the decorator as a mutex" example shows it, so this is a known consequence of the design: the check is a
GET, the write is aSET NXafter the action, and nothing marks the key as taken in between. But the case that matters most for an idempotency key is a client that timed out and retried while the original is still in flight, and that is precisely the window where the library runs the action twice. The architecture page also still says the pattern "ensures at-most-once or exactly-once semantics", which the measurement contradicts.What I think it needs is an in-flight reservation: before running the action,
SET NXa pending marker under the same key with a lease (say 30 s, configurable), then run, then replace the marker with the final record. A second caller that finds a pending marker either waits for the final record (poll with a bound, the lease) or fails fast with a newIdempotencyInProgressErrorthat an HTTP layer maps to 409, which is what Stripe does. An action that raises deletes the marker so the retry runs again, consistent with "failures are not cached". A marker whose lease expired is treated as absent, so a crashed worker cannot wedge a key. The mode belongs on the coordinator (in_flight="wait" | "raise", plus the lease); I lean towards reservation on by default in the next minor, with the old both-run behaviour available asin_flight="run"for callers whose action is genuinely safe to repeat, because a default that double-charges is the wrong default even when documented. Rule 2, the mutex example and the architecture page follow the code.Script:
measure.pyin https://github.com/bedrock-python/bedrock-python.github.io/tree/docs/production-python-series/docs/blog/lab/2026-09-07-idempotency-keys. Related: the fingerprint issue filed alongside this one.