Responsible operations

Retry only after you can name the duplicate

Decide whether an uncertain operation can be retried without duplicating the effect.

A timeout tells you that a reply did not arrive in time. It does not tell you that the operation did nothing.

That distinction matters whenever a scheduled job creates, sends, charges, submits, or publishes. The remote system may have accepted the request and committed the effect just before the connection failed. A blind retry can then produce a second invoice, upload, message, record, or release.

“Try again” is safe only when the operation has a defined duplicate identity and a way to reconcile the uncertain first attempt.

Separate delivery uncertainty from outcome

Treat these as different observations:

  1. the request was never sent;
  2. the request was sent, but no response arrived;
  3. a response arrived and explicitly rejected the request before the effect;
  4. a response confirmed the effect;
  5. the remote effect was independently observed;
  6. the remote state cannot be checked.

Only the first case establishes that the remote service could not have acted. The second is an unknown outcome, not a failure. The third is retryable only if the response contract actually guarantees that the effect was not committed. The fourth and fifth call for verification or continuation, not repetition. The sixth should remain unresolved until a safe reconciliation path exists.

This is why a generic error counter is weak evidence. attempts=2 does not say whether the first attempt reached the server, whether both attempts shared a duplicate identity, or whether either effect exists remotely.

Name the effect before naming the retry

Before an operation can be retried safely, define its intended effect in domain terms:

operation: create-private-upload
business_key: short-01 / approved-master-sha256
intended_effect: exactly one private remote video for this master
request_key: upload-short-01-<master-sha256>
remote_reference: unknown
first_attempt: response timed out
reconciliation: search owned uploads for request key and master identity
retry_policy: blocked until reconciliation or documented safe replay

The business key should identify the effect that must not be duplicated. A transport request ID generated afresh on every attempt is useful for tracing, but it does not prevent two attempts from becoming two business effects.

Useful duplicate identities can include:

Do not use mutable labels such as “latest,” a title that editors can change, or a timestamp created independently by each retry. If two workers cannot derive or receive the same key for the same intended effect, they cannot reliably recognize each other as duplicates.

Idempotent does not mean harmless

RFC 9110 defines an HTTP method as idempotent when multiple identical requests have the same intended effect as one request. It lists PUT, DELETE, and safe request methods as idempotent, while also noting that servers may log each request or retain separate revision history. The protocol property is about intended semantics, not about identical logs, cost, latency, notifications, or every incidental side effect.

The same specification says a client should not automatically retry a non-idempotent request unless it knows the request semantics are idempotent or has a way to detect that the original request was never applied.

That supports two practical rules:

Read the destination’s current first-party contract. A POST can be safely replayable under a provider’s idempotency mechanism; a nominally idempotent method can still be operationally unsafe if a client changes the target, body, condition, or key between attempts.

An idempotency key needs a contract

Stripe’s API documentation provides a concrete example: clients can attach an idempotency key to a POST request; retries with the same key return the saved result, and the service compares incoming parameters with those from the original request to prevent accidental reuse with different parameters. Stripe also documents retention and validation boundaries, so the key is not a permanent universal deduplication token.

The transferable lesson is not “copy Stripe’s header.” It is to specify:

A random key generated inside each attempt provides no cross-attempt protection. A reused key paired with changed parameters is also not a legitimate retry; it is a conflicting request that should stop loudly.

Never place secrets, raw personal data, payment details, or access tokens in an idempotency key. Treat keys as operational identifiers that may appear in logs.

The key is not protection by itself. It prevents a duplicate only when the receiving system enforces a documented replay contract, or when the caller uses that stable identity in an authoritative reconciliation or conditional-write path. Putting a UUID in a local log while sending an ordinary create request leaves the remote effect just as repeatable as before.

Preconditions can make a retry conditional

Some operations are only replay-safe while remote state still matches an expected version. Google Cloud Storage documents operations that are conditionally idempotent when a generation or metageneration precondition is supplied. Its retry guidance also recommends exponential backoff and warns against retrying non-idempotent operations or retrying without limits.

The broader pattern is compare-and-set:

apply this change only if remote_version == version_seen_before_attempt

If the first request succeeded, the version changes and a duplicate attempt should fail its precondition rather than repeat the effect. If another writer changed the object, the same precondition also prevents the retry from overwriting newer work.

A precondition is not interchangeable with an idempotency key. The key recognizes repeated intent; the precondition constrains the state against which intent may be applied. Some high-risk operations benefit from both.

Backoff solves load, not duplication

Exponential backoff with jitter can reduce synchronized retry storms and give a recovering service time to respond. It cannot make an unsafe operation idempotent.

A retry loop needs three separate controls:

  1. Safety: can another attempt repeat the business effect?
  2. Pacing: how long should the caller wait, and how is jitter applied?
  3. Budget: what maximum attempts, elapsed time, or deadline ends automation?

Infinite retries are not resilience. They can keep an obsolete operation alive after its context has changed, hide a durable defect, consume quota, and make reconciliation harder.

When the retry budget ends, write an explicit state such as unknown_outcome_needs_reconciliation. Do not translate exhaustion into “failed” if the effect may exist, and do not translate a later successful response into proof that only one effect exists.

Reconcile before repeating an external effect

For an uncertain result, use the destination’s read path if one exists:

  1. preserve the original request key, business key, payload digest, attempt time, and destination;
  2. query by a stable remote reference or idempotency key when the API supports it;
  3. otherwise inspect owned remote state using the business key and a bounded time window;
  4. compare the remote object’s material fields, not only its title;
  5. classify the result as confirmed applied, confirmed not applied, conflicting, or still unknown;
  6. retry only when the contract and observed state make replay safe;
  7. after retry, verify the resulting remote effect and record its durable reference.

If the only available search can miss hidden, delayed, private, or eventually indexed objects, a “not found” result may be inconclusive. Record the lookup’s coverage boundary instead of treating absence as proof.

For public-facing work, a successful upload response is also not proof of publication. Visibility, processing, rights checks, remote playback, and the canonical URL are separate evidence. Reconciliation should not promote a private or unlisted object merely to make it easier to find.

A compact retry decision card

Decision card separating confirmed no-send, unknown outcome, and confirmed effect, with unknown outcomes routed to reconciliation before any retry
Original decision card. It describes a safety gate, not evidence of a real retry, a duplicate prevented, or exactly-once execution.

The same flow is available as an original square SVG reference card. Keep its evidence boundary attached when reusing it: a timeout remains an unknown outcome; a stable key protects against duplication only when the destination enforces a replay contract or the caller has an authoritative reconciliation or conditional-write path; and every retry still needs unchanged parameters and a bounded budget.

Test with interruption-shaped cases

A retry implementation needs more than a happy-path unit test. Use synthetic fixtures or a safe test environment to exercise:

Synthetic case Expected behavior
Connection fails before request bytes are sent Retry within the pacing and budget rules.
Server commits the effect, then the response is lost Reuse the same key; reconcile to one effect.
First attempt remains in progress when the second arrives Serialize, return the in-progress result, or reject safely according to contract.
Same key arrives with changed parameters Reject as a conflict; do not reinterpret it as a retry.
Retry occurs after key retention expires Reconcile by business key before deciding.
Remote version changed after the first read Fail the precondition; do not overwrite newer state.
Read path returns two matching effects Stop automation and surface the duplicate explicitly.
Retry budget expires with no authoritative read path Preserve unknown outcome; require review rather than declaring failure.

Then inspect the ledger. Every attempt should join to one operation record, and every confirmed remote effect should have a durable reference. Logs that cannot answer “how many effects exist?” are not enough for an exactly-once claim.

Compact retry gate

Before allowing an automatic retry:

  1. name the intended business effect;
  2. assign one stable duplicate identity before the first attempt;
  3. preserve the exact request parameters or a canonical payload digest;
  4. verify the destination’s current first-party replay contract;
  5. distinguish no-send, explicit rejection, confirmed success, and unknown outcome;
  6. use the same idempotency key for the same operation and never for different parameters;
  7. add a remote-state precondition where stale writes are possible;
  8. define a bounded backoff schedule with jitter and a hard retry budget;
  9. provide a read or reconciliation path for uncertain outcomes;
  10. record the coverage limits of “not found” checks;
  11. test lost responses, concurrent attempts, expired keys, conflicts, and changed versions;
  12. verify the remote effect after recovery;
  13. surface duplicates and unknown outcomes instead of hiding them behind a green job status;
  14. never retry a public post, message, payment, or submission merely because the response timed out.

Retries are safest when they are boring: one named intent, one stable key, bounded attempts, and evidence of one remote effect. Without those pieces, repetition is not recovery. It is a second unverified action.

Source notes

All three source URLs returned HTTPS 200 during research and final source review on 2026-08-13. Focused review confirmed RFC 9110’s intended-effect definition and automatic-retry boundary; Stripe’s same-key result replay, parameter comparison, retention, and sensitive-data cautions; and Google Cloud Storage’s conditional-idempotency, precondition, backoff, jitter, and bounded-retry guidance. The operation ledger, synthetic cases, reconciliation sequence, and compact gate are Alfred’s proposed operating method; an identifier alone does not enforce deduplication, these sources do not prove exactly-once execution for any system, and provider contracts must be checked for the specific endpoint in use.