Idempotency & retries
Networks drop connections and SDKs retry. Neither should cost you twice.
Two protections run automatically. You do not opt in, and there is no header to set — which is the point: a safety net that requires configuration is one that protects only the people who already knew about the problem.
Duplicate blocking requests are replayed#
If the same API key sends a byte-identical request body twice within a short window, the second one replays the first result. No upstream call, no second charge.
- Matching is on content. A fingerprint of the key plus the exact request body. Any difference — a changed word, a different temperature — is a different request.
- It works with every client. Nothing is required from the caller, so curl, an SDK, a shell script and an agent framework all get the same protection. Header-based schemes only cover the clients that happen to send that header.
- The window is short — roughly two minutes. Long enough to absorb an automatic retry, short enough that deliberately asking the same question again later runs fresh.
Blocking requests only
Streaming is excluded on purpose. Interactive "regenerate" buttons send the same body deliberately, and the double-charge risk on a dropped stream is already covered by cancellation (below).
# First call — runs the model, billed once.
curl https://api.xkiro.com/v1/chat/completions \
-H "Authorization: Bearer $XKIRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"openai/gpt-5.6-sol","temperature":1,"messages":[{"role":"user","content":"Pick a number"}]}'
# Same body within ~2 minutes — replayed from cache, not billed again.
# Note it returns the identical id and content even at temperature 1.Failed requests are not cached#
Only a cleanly completed response is stored for replay. If a request fails, times out, or is cancelled part-way, the claim is released and your retry runs for real.
This matters more than it sounds. Caching a partial answer would mean a retry cheerfully returning a truncated response that looks successful — worse than the original failure, because nothing signals that anything went wrong.
Cancelling stops the charge#
When you close the connection — the user hits stop, the tab closes, the network drops — xKiro detects it and aborts the upstream request. You are billed for the tokens generated up to that point, not for the answer nobody received.
const controller = new AbortController();
// Anything that cancels: a stop button, a timeout, an unmounting component.
stopButton.onclick = () => controller.abort();
const stream = await client.chat.completions.create(
{ model: "openai/gpt-5.6-sol", messages, stream: true },
{ signal: controller.signal },
);Detection is server-side
You do not have to send anything. Even a process killed with Ctrl+C stops the upstream call, because the closed socket is the signal.
If you run a proxy#
One configuration mistake defeats all of this. If your load balancer retries POST requests on timeout, it sends a second request that xKiro sees as a legitimate separate call — from a different connection, at a different moment — and runs it.
location /v1/ {
proxy_pass https://api.xkiro.com;
# Never retry POST upstream: a retried inference request runs twice
# and is billed twice.
proxy_next_upstream off;
# Streaming must not be buffered, or the whole answer arrives at once.
proxy_buffering off;
# Allow long generations to finish.
proxy_read_timeout 300s;
}Writing your own retry logic#
- Retry only
429,500,502and503. See Errors. - Use exponential backoff with jitter. Retrying instantly recreates the spike that caused the failure.
- Cap attempts at three or four. Beyond that you are usually queueing behind a real outage, and each attempt adds latency your user is waiting through.
- Do not change the body between retries if you want deduplication to protect you — a regenerated request ID or timestamp inside the body makes each attempt look new.
