Operations / Correct under repetition
Retrying a render safely, and which failures are safe to retry
Deciding which failures should be retried and which should not, because a blanket retry on a document endpoint is a duplication mechanism.
Why the default answer is wrong here
A retry is safe when the operation is idempotent and pointless when the failure is deterministic. Document pipelines routinely get both wrong: they retry a validation error that will fail identically every time, burning attempts and delaying the real error, and they retry a timeout without idempotency, which is the case most likely to have partially succeeded. The distinction is knowable from the error, because the failures fall into clear classes.
The decisions
Reasons rather than a description of the code. Each has a default that is defensible in general and wrong for documents specifically.
- Retry transient failures only: a timeout, a renderer that was unavailable, a storage backend that was briefly unreachable, a rate limit. Those recover on their own.
- Never retry a request rejection. An invalid request, a payload that was too large, an unauthorized call and an expired key all fail identically on the second attempt, and retrying them delays the error somebody needs to see.
- Retry with exponential backoff and jitter, because a fleet retrying in lockstep after a shared failure produces a second outage on recovery.
- Honour Retry-After when it is present rather than using your own interval, since the server is telling you when it will be ready.
- Cap the total attempts and the total elapsed time, and make the give-up path do something visible rather than swallowing the failure.
- Only retry at all if the operation is idempotent by the previous page's definition. If it is not, fix that first, because retry without idempotency is a duplicate generator with a delay built in.
In practice
A fragment, with the thing that goes wrong kept in a comment where it is the more instructive half.
// The classes are knowable from the error, so the decision is a lookup
// rather than a judgement call at the call site.
const TRANSIENT = new Set([
"timeout",
"renderer_unavailable",
"storage_unavailable",
"db_error",
]);
const PERMANENT = new Set([
"invalid_request", // fails identically every time
"payload_too_large",
"unauthorized",
"key_expired",
"plan_required",
"ssrf_blocked",
"resource_load_failed", // the asset URL is wrong; retrying will not fix it
]);
async function renderWithRetry(body, { attempts = 4 } = {}) {
for (let attempt = 1; ; attempt += 1) {
try {
return await pdf.post("/v1/pdf", body);
} catch (err) {
if (PERMANENT.has(err.code)) throw err; // surface it now
if (!TRANSIENT.has(err.code) && err.status !== 429) throw err;
if (attempt >= attempts) throw err;
// The server's own answer beats our guess.
const retryAfter = Number(err.headers?.["retry-after"]);
const wait = Number.isFinite(retryAfter)
? retryAfter * 1000
: Math.min(2 ** attempt * 250, 8000) * (0.5 + Math.random());
await sleep(wait); // jitter, so a fleet does not retry in lockstep
}
}
}What people do instead
A generic HTTP client retry policy applied to the document endpoint. It retries everything including the permanent failures, so a malformed template produces four identical rejections and an error message four times slower than it needed to be, and a timeout produces two documents.
What the symptom looks like
A rising retry count with a stable success rate means something upstream is slow rather than broken. A rising retry count on permanent codes means the retry policy is misconfigured, and it is worth counting retries by error code for exactly that reason.
Frequently asked
Is this worth doing for a small volume of documents?
Some of it, and the cheap parts are the ones that matter. Allocating a document's identity before rendering it costs nothing and prevents duplicates forever. Logging the template version costs one field and answers most support questions. Queue design, backpressure and capacity planning are genuinely for scale and can wait until there is some.
Why is duplication treated as more serious than latency here?
Because a document usually carries an identity. A duplicated read is harmless and a duplicated invoice is a second numbered document for one event, which somebody has to reconcile by hand and which may already have been sent. That asymmetry is why the correctness half of this cluster is larger than it would be for most APIs.
How does this relate to the troubleshooting pages?
Those start from a symptom you are looking at right now and work back to a cause. These start from a decision made before the symptom exists. The two meet in the middle: a decision skipped here usually appears there some months later as a problem with no obvious explanation.
Related operational topics
The decisions that depend on each other, then the rest of the same group.
Idempotency for a document that must not be generated twice
Making a repeated request produce the same document rather than a second one, which matters more here than in most APIs because the artefact is often numbered.
Designing a client that lives inside a rate limit
Building the caller so it stays within a limit rather than discovering it, which is a different question from what to do when a 429 arrives.
Setting timeouts along the whole path, not just in the client
Making the timeouts at each hop consistent with each other, because a path whose timeouts disagree fails in the least useful way available.
Stopping the same document being requested twice at once
Collapsing concurrent identical requests, which is a different problem from idempotency because both arrive before either has finished.
Caching a rendered document, and when it is safe to serve one
Serving a previously rendered document instead of making a new one, which is nearly free when the inputs are unchanged and wrong when they are not.
Every operational topic
The full list, grouped by correctness, visibility and cost.
What this API actually does
The options and endpoints these decisions are built on, one page each.
Most of these decisions are cheaper to make before the first production run than after the first incident, and none of them need a large system to be worth making.