Retrying safely
- Updated
- Reading time
- 2 min
- Level
- beginner
Send an Idempotency-Key header and a retry becomes free. Same key with the same body returns
the original job, in whatever state it is in, instead of starting a second one.
POST /v1/completions
Authorization: Bearer revoye_sk_live_…
Idempotency-Key: 4f1c9d2e-8b3a-4c1d-9e2f-7a6b5c4d3e2fThe rules
| Situation | Result |
|---|---|
| Same key, same body | The original job is returned, whatever state it is in |
| Same key, different body | 409 CONFLICT. Silently returning the first result would be worse than an error |
| No key | Every request creates a new job |
Keys are scoped to your account and retained for 24 hours.
Why this matters more here than elsewhere
A Revoye job runs for tens of seconds inside wait: true. That is a long time for a connection to
survive a load balancer, a proxy, a VPN, or a laptop lid. When one of those drops the connection, you
have no idea whether the job was accepted.
Without a key, your retry starts a second job — a second agent, a second slot against your hourly limit, and a second answer you did not want. With a key, your retry finds the first one.
Generating keys
Use a UUID per logical operation, not per HTTP attempt:
key = str(uuid.uuid4()) # once, before the first attempt
for attempt in range(3):
try:
return post("/v1/completions", headers={"Idempotency-Key": key}, json=body)
except (httpx.TimeoutException, httpx.NetworkError):
continueRegenerating the key inside the retry loop defeats the whole mechanism — that is the mistake to look for when a retry produces duplicate work.
If your work already has a natural unique identifier — a row id, a message id, a job id from your own queue — derive the key from it. Then even a process that crashes and restarts retries correctly, because it recomputes the same key instead of remembering one.
The 409 is telling you something real
CONFLICT means you reused a key with a different body. Almost always this is a bug in your key
derivation — two different pieces of work computing the same key. Fix the derivation; do not retry
with a fresh key and move on, because the collision will happen again with two jobs that matter.
When you do not need one
GET and DELETE are already idempotent. The header is for POST /v1/completions and
POST /v1/chat/completions.