Timeouts, retries and idempotency
How long a call waits, when it is retried, and what an idempotency key buys you.
One attempt may take timeout milliseconds (default 60000). A failed attempt is retried up to maxRetries times (default 2, so three attempts), on the same terms for every call.
Every operation that creates or consumes something carries an Idempotency-Key the SDK generates for it, so a repeated attempt is deduped by the server rather than acting twice; the rest are idempotent already.
What is retried
Retried:
- A connection failure, or a timeout, including while reading a response body.
- A
5xx, a429, or a408.
Not retried:
- Any
4xx, a409among them: the request itself needs changing. A repeat the server is still executing under the same key waits on its own lock rather than answering, so there is nothing here for a retry to pick up. - A request whose body is a
ReadableStream, which cannot be read twice.
A 401 is the one refusal handled outside this budget: with a function credential the token is resolved again and the request goes once more, spending no retry. See Clients and authentication.
The wait honours Retry-After when the server sends one, and is otherwise exponential with jitter.
Setting them
Both settings can be set on the client and overridden per call.
const subako = new SubakoClient({
baseUrl,
apiKey,
timeout: 10_000,
maxRetries: 4,
});
await subako.agents.list(undefined, { maxRetries: 0 });
Idempotency keys
The generated key covers the SDK’s own retries. Pass idempotencyKey to dedupe across your retries too, or across processes: the same key with the same request returns the first attempt’s receipt instead of acting again, for at least seven days after it completed.
await subako.sessions.create({ agent_id: agentId }, { idempotencyKey: `nightly-${date}` });
A key is sent only where the operation takes one, and is ignored elsewhere. Reusing one key for a different request is a 409, so a ConflictError no retry will settle.
Receipts
Three operations answer with a secret shown once. A replay never carries that secret again, so each of them answers with either the fresh body or a receipt, and the type says so:
| Operation | Fresh (201) |
Replayed (200) |
|---|---|---|
sessions.create |
CreatedSessionBody |
CreatedSessionReceiptBody |
sessions.mintToken |
SessionTokenBody |
SessionTokenReceiptBody |
apiKeys.mint |
MintedApiKeyBody |
ApiKeyReceiptBody |
The receipt confirms what the first attempt committed — the session and its id, the key and its prefix — and drops the field carrying the secret, which is what narrows the union:
const created = await subako.sessions.create({ agent_id: agentId });
if ("session_token" in created) {
use(created.session_token);
} else {
// The first attempt committed and its answer was lost: the session is there
// under `created.id`, and a fresh mint gives it a token.
const minted = await subako.sessions.mintToken(created.id);
if ("session_token" in minted) {
use(minted.session_token);
}
}
A receipt reaches you only where a key was spent twice: the SDK’s own retry of a lost response, or an idempotencyKey you repeated yourself. A first attempt under a fresh key always answers with the secret.