Skip to content

Two concurrent callers with the same key both run the action: an in-flight reservation is needed #26

Description

@AlexeyShalaev

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions