Gopher Systems Journal
Independent Go engineering notes · Daily edition
Distributed Systems · APIs

Idempotency for Go APIs That Retry in the Real World

Keys, state machines, and response replay for safely repeating side-effecting requests.

August 22, 20269 min readIssue 0686

Retries are part of the protocol

A client can lose the response after the server commits. From the client's perspective the operation is unknown, not failed. Idempotency gives both sides a way to resolve that uncertainty. The key must identify one logical operation, be scoped to an authenticated actor, and expire according to a documented window.

type Record struct {
    Key         string
    RequestHash [32]byte
    State       string // started, completed, failed
    Status      int
    Response    []byte
}

Reject key reuse with different input

Store a canonical request hash with the key. The same key and same input may replay a completed result; the same key with different input is a conflict. Create the idempotency record and business mutation in one database transaction where possible. A “started” state also needs a recovery rule for processes that die mid-operation.

Do not hold an HTTP connection or database lock while waiting indefinitely for a duplicate request. Return a clear in-progress response or wait within the caller's deadline. Decide whether failures are replayed: permanent validation failures usually can be, while transient infrastructure failures may allow a fresh attempt.

Test uncertainty, not only duplicates

Drop the first response after commit, run concurrent requests with one key, restart between state transitions, and reuse a key with altered input. Observe deduplication hits and stale in-progress records. The happy path proves little; idempotency exists for the ambiguous path.

Today's implementation checklist

Before changing production code, write down the resource being protected, the failure signal a caller will see, and the metric that will prove improvement. Prefer a small reversible change. Exercise cancellation and overload paths, then review the resulting profile or trace beside the baseline. The discipline is intentionally plain: make ownership visible, put a bound on waiting, and preserve enough evidence to explain the outcome tomorrow.