Retries and Idempotency
What the SDK retries on its own, how to tune it, how to make writes safe to repeat, and when to add an outer retry.
What the SDK retries automatically
Every Client call (the namespaces and client.raw.invoke) and every event post on the loop lane is retried up to max_retries times with exponential backoff and jitter when the failure is one of these:
RateLimitError(429) andUnavailableError(503).InternalServerError:500and any other5xx(UnavailableErroris one, so503is covered twice).TransportErrorwhosecodeisUNAVAILABLE,DEADLINE_EXCEEDED,RESOURCE_EXHAUSTED,ABORTED,INTERNAL,CANCELLED,CONNECTION_ERRORorTIMEOUT: a request or connect timeout, a dropped connection, a gRPC transient code.
It never retries:
- A refused connection or an unresolvable host (
APIConnectionError.refusedisTrue): nothing is listening, so the call fails withinconnect_timeout(2 s) on the first attempt. 400,401,403,404,409,410(BadRequestError,AuthenticationError,PermissionDeniedError,NotFoundError,AlreadyExistsError,GoneError),UnsupportedFeatureErrorandLessonRejected: caller errors; retrying repeats them.
The last error is re-raised when the retries are exhausted, so a handler sees the real class (see Errors), never a wrapper.
Tuning
| Setting | Env var | Default | Meaning |
|---|---|---|---|
max_retries / maxRetries | MUBIT_MAX_RETRIES | 2 | Retries after the first attempt (0 disables). Per client on Client(max_retries=...), client.with_options(max_retries=...), JS new Client({ maxRetries }) / withOptions({ maxRetries }) |
timeout / timeoutMs | MUBIT_TIMEOUT_MS | 30000 | Request timeout in ms (Client(timeout=30.0) takes seconds) |
connect_timeout / connectTimeoutMs | MUBIT_CONNECT_TIMEOUT_MS | 2000 | Connect timeout, and the gRPC readiness wait before transport="auto" falls back to HTTP |
| — | MUBIT_RETRY_BASE_MS | 200 | Base delay in ms (minimum 10) |
| — | MUBIT_RETRY_CAP_MS | 5000 | Maximum delay per retry |
| — | MUBIT_RETRY_JITTER | 0.2 | ± jitter fraction (0.0 disables jitter) |
The delay before attempt n (the first retry is attempt 2) is min(base × 2^(n−2), cap), then scaled by a random factor in [1 − jitter, 1 + jitter]: with the defaults, about 200 ms, 400 ms, 800 ms, ... capped at 5 s. MUBIT_RETRY_ATTEMPTS (total attempts including the first) is the deprecated 0.13 name: it is read only when MUBIT_MAX_RETRIES is unset, equals MUBIT_MAX_RETRIES + 1, prints one deprecation notice, and is removed in 1.0.
MUBIT_MAX_RETRIES=4
MUBIT_RETRY_BASE_MS=500
MUBIT_CONNECT_TIMEOUT_MS=5000client = mubit.Client(max_retries=0) # fail fast in a request handler
worker = client.with_options(max_retries=5, timeout=60.0)Idempotency keys
client.memory.remember() (and the underlying control.ingest) carry an idempotency key so a repeated write returns the existing entry instead of creating a duplicate. If you don't pass one, the key defaults to the item id (item_id, else an auto-generated remember-<timestamp>). Pin it explicitly to dedupe across retries from a queue worker:
client.memory.remember(
"…",
kind="fact",
idempotency_key=f"ticket-{ticket_id}-fact-1",
run_id=run_id,
agent_id="support-agent",
)client.outcomes.record(reference_id=..., idempotency_key=...) applies the outcome at most once per key, so a retried outcome write reinforces once rather than double-counting. Loop events (run.start, model.call, outcome, run.end) carry a content-derived event_id; a re-posted batch is reported under duplicates and applied once. Other writes are naturally idempotent by their own ids (client.memory.archive keys on the block id, client.agents.register on the agent id).
When to retry yourself
The built-in retries cover a transient blip. Add an outer budget only in queue workers and batch jobs that can wait longer than a few seconds, and only around the retryable classes:
- Retry
RateLimitError(honourretry_afterwhen present),InternalServerError/UnavailableError, andAPIConnectionErrorunlessrefusedisTrue. - Don't retry
BadRequestError(which includesNotFoundErrorandAlreadyExistsError),AuthenticationError(which includesPermissionDeniedError),GoneError,UnsupportedFeatureError,LessonRejected: fix the call instead.
The 0.13 handlers (except ServerError, except TransportError) keep working: ServerError is InternalServerError and TransportError is an APIConnectionError. They do not see RateLimitError (429), which has no 0.13 parent; add it when you move to the outer-retry pattern.
See also
- Errors — status codes and the SDK exception hierarchy
- SDK configuration — timeouts, transport selection and the failure policy
- Rate limits — input caps and overload behavior