Engineering2 min read

An idempotency key has an owner

Replaying a key returns the same job. Sending someone else's now returns 409 instead of quietly handing it over.

An Idempotency-Key exists so that a retry is safe. You send one with a job, the network times out, you retry with the same key, and you get the same job back instead of a second charge.

That part worked. The part that did not was what happened when the key came from somewhere else.

#The shape of the bug

Idempotency is usually described as a property of a request: same key, same result. What that description leaves out is whose key it is.

Consider two services in the same account, each with its own API key, each generating idempotency keys from something that felt unique enough — a job id from an internal queue, say, or a hash of the prompt. Sooner or later they collide.

The old behaviour was to look up the idempotency key, find a job, and return it. Both services got a 200. Both believed they had submitted work. One of them received the other's render.

Nothing errored. Nothing was logged. The second service's job simply never existed, and the evidence that it never existed was a successful response describing a job that looked exactly like the one it had asked for.

#What it does now

An idempotency key belongs to the API key that first used it.

409 Conflict
{
  "error": "idempotency_key_owned",
  "docs": "https://auray.ai/docs/api/errors#idempotency_key_owned",
  "request_id": "54b4754a-3e17-4629-adb1-7d3f067e97d3"
}

Same API key, same idempotency key: the original job, at whatever status it has now, and no second charge. Different API key: refused. There is no third outcome.

A 409 is not a nicer failure than a 200 — it is a visible one. The collision was always happening; the only question was whether anybody found out.

#Why this is the right trade

The obvious objection is that a 409 breaks a caller who was previously fine. It does, and that caller was fine by luck: it was reading another service's job and would have kept doing so until somebody noticed the renders did not match the prompts.

The second objection is that scoping keys per API key makes them less useful across a fleet. That is true and it is the point. If two processes genuinely need to share an idempotency key, they should share the API key that owns it — which is a decision somebody makes on purpose, rather than a collision they discover in production.

#What to do on your side

Generate an idempotency key per submit, not per user and not per prompt. A UUID is the simple answer:

curl -X POST https://queue.auray.run/auray-ai/krea-2-turbo/text-to-image \
  -H "Authorization: Bearer $AURAY_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "prompt": "…", "aspect_ratio": "16:9", "resolution": 1024 }'

A key derived from something stable will collide the first time the same input is submitted twice on purpose — and then hand back the first job instead of generating anything.

Full detail in Idempotency.

Filed under Engineering on All posts

Build on what you just read

Open the app to try the generators, or read the API reference and call them from your own code — one key, one balance of credits.