ADR-018. A retry policy belongs to the operation, and an action is never replayed

Status: accepted, 8 September 2026. Still in force.

Context

The runtime had no retry at all: one request, one response. On a cloud that makes a playbook fragile for nothing, because a 503 lasting thirty seconds fails an entire deployment.

The obvious fix is the wrong one. A Retry(total=5) on a requests session, or any policy held by the client, applies the same rule to a GET and to the POST that reboots a machine. A reboot played twice is not a reboot. The policy has to belong to the operation, the way a comparison strategy belongs to the field (ADR-009).

What the policy can be derived from is narrow, and that is deliberate. No Scaleway document declares an operation idempotent, none carries an idempotency key, and none says which failures are safe to replay. Two facts remain, and both are stated rather than guessed: the HTTP method, whose safety RFC 9110 fixes, and the class the classifier established.

The families of failure are not equivalent either:

  • a 429 is the rate limiter answering before the API. The request was not processed, so replaying it changes nothing that has not already been changed;

  • a 500, 502, 503 or 504 is ambiguous. After one of them, a write may have landed and only a read could tell;

  • a transport failure, a timeout or a refused connection, is the most ambiguous of all: the request may have left.

Decision

Every operation carries a retry policy, and the generator derives it from the HTTP method and the operation class. Three values, and the class is tested first because ServerAction is a POST exactly as a creation would be:

policy

which operations

replayed on

safe

GET, so every read

429, the ambiguous 5xx, transport failures

limited

the writes of a managing module

429 only

never

the operation an action module triggers

nothing

limited does not replay an ambiguous failure. After a 502 the write may have landed, and this runtime prefers to fail loudly rather than report a state it did not observe, which is the same rule the postcondition check applies (ADR-010).

Retry-After is honoured, in both forms RFC 9110 allows, a number of seconds or an HTTP date. Reading only one of them would ignore the other in silence and replay too early against an API that just said no. An unreadable header falls back to the default wait rather than failing the module: the server already said the essential by answering 429.

A Retry-After beyond the ceiling is not waited for. The server can announce minutes; obeying blindly would make a module that looks frozen, and the error that comes out carries the header in its message.

The runtime default is never. A module produced by an older generator, declaring nothing, must not start replaying writes because this runtime learned how.

The retry lives around the single HTTP call, and nowhere else. Putting it in request would make every caller responsible for knowing whether it had already been replayed; putting it in a requests session would make it global, hence identical for a read and for a reboot, which is what this record refuses.

Consequences

An operation that deserves a different policy than the derivation gives has no way to say so yet. depuis_le_nom exists and refuses an invented value, so an override can be wired the day a case appears; until then, adding the plumbing would be building for a case nobody has met.

The waits are a plain increasing sequence rather than an exponential backoff with jitter. The exact values are not what matters here, and a formula nobody can read is a formula nobody can judge. If a measurement ever shows the sequence matters, it will be changed with that measurement.

A limited write that fails on a 503 fails the playbook where a replay might have succeeded. That is the price, and it is the direction this repository errs in everywhere else: a module that reports what it did not observe is worse than a module that fails.

Nothing here was measured against a real 429 or a real 503 from Scaleway: the behaviour on those codes is derived from RFC 9110 and from what the codes mean, not from an observation. The tests fabricate the responses.

What this record does not decide

Whether a failed write should be reconciled by re-reading and comparing before deciding to replay. That would be a stronger guarantee than failing, and it needs the read to be cheap and the comparison to be exact for every managed field. ADR-010 already reads after a successful write; extending it to an ambiguous failure is a separate decision.