← Articles

A timeout after the write: retry without a duplicate effect

The response was lost, but the booking may exist. Explore idempotency keys, conflicting input and the limits of a small example.

Underlined terms have explanations. Copy the code and run it locally.

Contents and terms

Terms at hand

Select a term to see its definition.

Full glossary →

A timeout describes the wait, not the outcome

You click “Reserve”. A few seconds later, the request times out. Can you safely click again? The server may have written nothing, or it may have completed the write and lost the response. Those situations look identical to the client.

Before adding , answer a different question: how do you recognize the same user intent? Repeating an request does not answer that question. For an operation that creates a booking, generating a fresh on every attempt may create a fresh booking.

Diagram

Rendering diagram…

Diagram source
sequenceDiagram
    participant C as Client
    participant A as API
    C->>A: Booking, key attempt-42
    A->>A: Store result
    A--xC: Response lost
    C->>A: Retry, same key
    A->>C: Same result

A small runnable example

Here, means that repeating the same operation does not create another effect. Save this as retry.py and run python3 retry.py ( 3.11+). No or network is needed. This is deliberately a single-process model of the rule, not a complete payment or reservation mechanism.

python
responses: dict[str, tuple[int, str]] = {}
created: list[str] = []

def create_booking(key: str, seats: int) -> str:
    if not key or type(seats) is not int or seats < 1:
        raise ValueError("invalid input")
    if key in responses:
        previous_seats, booking_id = responses[key]
        if previous_seats != seats:
            raise ValueError("key reused with different input")
        return booking_id
    booking_id = f"booking-{len(created) + 1}"
    created.append(booking_id)
    responses[key] = (seats, booking_id)
    return booking_id

first = create_booking("attempt-42", 2)  # Imagine this response was lost.
second = create_booking("attempt-42", 2)
assert first == second
assert len(created) == 1
print(second, len(created))
try:
    create_booking("attempt-42", 3)
except ValueError as error:
    print(error)
else:
    raise AssertionError("Conflicting input must be rejected")

Expected output:

text
booking-1 1
key reused with different input

The first call creates a result and it under the key. The second returns that result. If the same key arrives with a different seat count, we reject the conflicting input instead of pretending it repeats the previous intent.

Change one thing and observe the difference

Change attempt-42 to attempt-43 in the second call. The first == second should fail. You have demonstrated the distinction between retrying an operation and starting a new operation. the key. Now consider when the client should create it and how to preserve it between attempts.

A key does not replace . In a real system, its scope must include the user or and operation type. Knowing someone else's key must not reveal their booking.

Where this example stops being sufficient

An in-memory dictionary disappears on restart. Two processes have two dictionaries. requests can both see a missing entry and both perform the effect. A production implementation therefore needs durable storage and atomic conflict protection, not just an if statement.

Also distinguish a local write from a call to an external provider. A transaction in your database cannot undo a payment already collected elsewhere. Understand the provider's contract, status lookup and retry rules. Do not assume every key-bearing header offers identical guarantees.

Decisions to make before deployment

  • What identifies the intent, and who preserves the key between attempts?
  • What happens when the same key has different input?
  • What happens under concurrent requests and after restart?
  • How long is the result retained, and what does the client do afterwards?
  • What is the retry limit, and how do you recognize an unknown outcome?

Work through these questions for one operation before adding a generic retry mechanism across the application. The important evidence is a single effect, not a count of successful HTTP responses.

Sources

CHECK YOUR INTUITION

The response was lost. Which key represents the same intent?

NOW WRITE YOUR OWN CODE

Put your understanding into practice.

In the Java module, analyze a payment, an unknown outcome and compensation.

Open the learning material →