All posts
retriesidempotencyobservabilitydistributed systems

Separate request identity from attempt identity

Retries stay auditable when the customer's command keeps one identity and every execution receives another

A customer asks a system to create one shipment.

The first call times out. The client tries again. A queue worker retries twice. An operator later performs a manual replay.

How many requests exist?

From the customer's point of view, there is one command. From the execution system's point of view, there are several attempts. Giving both layers the same identifier collapses two different facts and makes the incident harder to reconstruct.

The durable model uses one request identity for intent and a new attempt identity for every execution. The request answers what the customer wanted once. The attempt answers what the system tried at a particular time.

One identifier cannot explain both intent and execution

An idempotency key is usually meant to preserve logical intent across retries. If a client sends the same command again after losing the response, the server can recognise that the customer is not asking for a second shipment.

That key is weak as an execution identifier. Several workers may process the logical request at different times. A provider call may be retried. An operator may initiate recovery after checking remote state. Each attempt needs its own timestamps, error boundary, worker, configuration, and result.

If every attempt writes logs under only the idempotency key, the timeline becomes ambiguous. Two started events may look duplicated. A late success may be mistaken for the response to the latest retry. A manual replay may overwrite the error that justified intervention.

The reverse mistake is just as damaging. If each retry creates a wholly new request identity, the system loses the connection to the original intent. Duplicate prevention becomes a best-effort log search, and customer support sees several apparent commands instead of one unresolved case.

Model the relationship directly

A minimal record can keep the distinction explicit:

request_id: req_7f31
idempotency_key: customer-supplied-key
desired_operation: create_shipment
current_outcome: unknown

attempt_id: att_01
request_id: req_7f31
trigger: initial
result: provider_timeout

attempt_id: att_02
request_id: req_7f31
trigger: automatic_retry
result: provider_lookup_found_receipt

The request owns the stable customer-facing state. The attempt owns the mechanics of one execution. Attempts append to the request rather than replacing it.

This separation also clarifies what may change. The request payload should usually remain immutable after admission, apart from carefully controlled metadata. An attempt may use a different worker, start time, timeout budget, or recovery path. If the business command itself changes, that is normally a new request, not another attempt at the old one.

Status belongs at both levels

A single status field often creates misleading transitions. The first attempt fails, so the request becomes failed. A retry begins, so it becomes processing again. A delayed provider receipt arrives, so it becomes succeeded. The history survives only in logs, while the main row tells a story that keeps rewriting itself.

Store attempt status and request outcome separately.

An attempt can be timed out while the request outcome remains unknown. Another attempt can complete a lookup without executing the business operation. The request can become confirmed complete when reliable evidence arrives, even if the attempt that caused the side effect never returned successfully.

This is not extra ceremony. It prevents the transport result from impersonating the business result.

Useful request outcomes might include pending, confirmed complete, confirmed absent, cancelled, and unknown requiring reconciliation. Useful attempt outcomes describe execution facts such as accepted, rejected, timed out, transport failed, lookup found, and lookup absent. The exact vocabulary matters less than keeping the two levels honest.

Tracing should preserve the same split

Distributed tracing often introduces a fresh trace for each inbound call. That is useful for one execution path, but a customer retry may therefore create a new trace even though it belongs to the same logical request.

Put the stable request identity and the unique attempt identity into logs, traces, queue metadata, and provider call records. Then an engineer can inspect one attempt in detail or gather every attempt attached to the request.

The identifiers answer different operational questions:

  • Which attempt exhausted its timeout budget?
  • Which attempts reached the provider?
  • Did two workers execute the same request concurrently?
  • Which evidence established the final request outcome?
  • Was the manual replay a new attempt at unchanged intent?

Without both identities, these questions turn into timestamp comparison and guesswork.

Manual recovery must append, never disguise

A replay button is especially likely to blur the model. Moving the original job back to ready may reuse its job identifier and make the new execution look like a continuation of the old one. Creating an unrelated job may lose the link to the customer's command.

Manual recovery should create a new attempt attached to the same request. The attempt records who authorised it, which evidence made it eligible, and whether any parameters changed. The original failed or uncertain attempt remains intact.

If recovery changes the intended operation, stop calling it a replay. A corrected address, different payment amount, or revised notification audience represents new intent. Create a new request and link it to the superseded one so the relationship remains visible without pretending they are identical.

Metrics become more trustworthy

Mixing requests and attempts distorts operational measures. A service that receives one customer command and performs four retries has not served four requests. It has handled one request badly through five attempts.

Count customer intent at the request level. Measure execution cost and reliability at the attempt level. Then retry amplification, attempts per request, time to confirmed outcome, and requests left unknown become visible without inflating business volume.

This distinction also improves service-level reporting. Request success rate can describe whether customers received a confirmed outcome. Attempt success rate can reveal a fragile dependency that eventually succeeds only after repeated work. Both numbers matter, and neither should be asked to stand in for the other.

Stack Dispatch treats retries as part of a durable case history. Keep the customer's command under one stable identity. Give every execution a fresh attempt identity. Link the evidence between them. When something times out, arrives late, or needs manual recovery, the system can then explain exactly what was wanted, what was tried, and which fact finally closed the request.

0 comments

Join the conversation

Get the next dispatch

New writing on software architecture, AI systems, and shipping production software, sent by email. Unsubscribe anytime.