# Outcome-unknown recovery when a write times out

## TL;DR

A timed-out write has three possible states, not two: committed, not committed, and unknown. Never guess. If the API reports `commit_state` of `not_committed`, retry freely. If it reports `unknown`, replay the identical request under the same idempotency key so the server returns the original outcome. If the transport died before any response, read back the target state first, then replay identically if the write is still missing.

## When to use this

- A POST or PUT timed out, the connection dropped mid-response, or a proxy returned 503 with no application body.
- A worker crashed between sending a write and recording the result.
- A response arrived truncated, so you hold an identifier you cannot trust yet.

## When not to use it

- The server returned a definitive 4xx validation error. Nothing committed; fix the request and send it as a new write with a new key.
- The failed call was a read. Just retry the read.

## Steps

1. Freeze the attempt. Record the exact payload bytes, the idempotency key, the endpoint, and the time. Success check: you can reconstruct the identical request without rebuilding it from memory.

2. Read the error surface before retrying anything. Vectle-style APIs report `error.code`, `error.retry.kind`, `error.retry.after_seconds`, and `meta.commit_state`. Follow the stated retry kind: `retry_identical`, `wait_then_retry`, `fix_request`, `provide_credential`, or `none`. Success check: your next action matches the server's instruction, not a generic backoff habit.

3. If commit state is `not_committed`, the write did not happen. Retry under the documented rule for that error, usually the same key while the attempt is still the same logical write.

4. If commit state is `unknown`, resend the identical request with the same idempotency key, byte for byte. Expected behavior: the server replays the stored outcome, returning the original success or the original failure, without applying the write twice.

5. If you never received a response, read back first. Fetch the target object or list by a stable attribute from the payload. If it exists with the right content, adopt it and record its identifier. If it does not exist, replay identically under the same key.

6. Verify whatever identifier you end up with. Read the object through the API and, for public content, fetch the public page. A replayed identifier can be a phantom if the original commit actually failed downstream; only count the write as done after the read-back matches.

## Variants

### The 503 with no body

An edge or load balancer 503 without an application body tells you nothing about commit state. Treat it as unknown: read back, then identical replay. Do not alternate payloads hoping one lands.

### Partial success in a batch

In a multi-item write, recover per item. Committed items keep their results; unknown items replay individually under their own keys. Re-running the whole batch under one key hides which items duplicated.

## Why guessing is the failure mode

Retrying with changes assumes not-committed and creates duplicates when the first write landed. Walking away assumes committed and silently loses the write when it did not. The identical replay is the only move that is safe in both worlds, because the server, not the client, resolves the ambiguity.

## Edge cases

- Never change the payload under the same key to fix a validation error you noticed later. That converts a recovery into a key conflict or, worse, a silent mismatch.
- Idempotency keys expire. For a very old unknown write, read-back is the only reliable check; a replay after expiry is a new write.
- Long backoffs between replays are safe; the key, not the timing, carries the deduplication.
- Log the outcome source (original response, replay, or read-back adoption) with the result. Future debugging depends on knowing which path produced the identifier.
