Retries and cancellation
Configure the HTTP client's built-in retry policy, retry wire calls safely with the request-id contract, and bound calls with timeouts — the SDKs take no external AbortSignal.
- A Brain client.
- Familiarity with Error handling.
- A latency budget you want calls to respect.
A configuration that retries only what is safe, backs off with jitter, honors a server hint, and fails fast when a deadline is blown.
What retries automatically, and what doesn't
The two clients wire retry differently:
Step-by-step
The policy is { maxAttempts, baseDelayMs, maxDelayMs }. Backoff doubles from baseDelayMs, capped at maxDelayMs; a server Retry-After header overrides the schedule.
import { BrainHttpClient, NO_HTTP_RETRY } from "@brain-db/sdk";
// DEFAULT: { maxAttempts: 3, baseDelayMs: 100, maxDelayMs: 2000 }
const brain = new BrainHttpClient({
apiKey: process.env.BRAIN_API_KEY!,
retry: { maxAttempts: 5 }, // merged over the default
});
// Opt out entirely (single attempt):
const once = new BrainHttpClient({ apiKey: "…", retry: NO_HTTP_RETRY });from brain_db_sdk import BrainHttpClient
from brain_db_sdk.http.retry import HttpRetryPolicy
brain = BrainHttpClient(
os.environ["BRAIN_API_KEY"],
retry=HttpRetryPolicy(max_attempts=5),
)use std::time::Duration;
use brain_db_sdk::{BrainHttpClient, HttpRetryPolicy};
// DEFAULT: 3 attempts, 100 ms base backoff, 2 s cap.
let brain = BrainHttpClient::new("https://api.arc-labs.ai", &std::env::var("BRAIN_API_KEY")?)
.with_retry_policy(HttpRetryPolicy::new(5, Duration::from_millis(100), Duration::from_secs(2)));
// Opt out entirely (single attempt):
let once = BrainHttpClient::localhost("…").with_retry_policy(HttpRetryPolicy::none());The wire client does not retry for you. Build the request once so its requestId is stable, then re-run while the error is retryable. Reusing the same request object is what keeps the resend idempotent — a rebuild mints a new id and defeats it.
import { ForgetBuilder, isRetryable } from "@brain-db/sdk";
const req = new ForgetBuilder(memoryId).build(); // build once → stable requestId
for (let attempt = 1; ; attempt++) {
try {
await client.forget(req);
break;
} catch (e) {
if (!isRetryable(e) || attempt >= 3) throw e;
await new Promise((r) => setTimeout(r, 100 * 2 ** (attempt - 1)));
}
}The SDK also ships a withRetry combinator that wraps this loop with jittered
backoff — see Retries.
from brain_db_sdk import ForgetBuilder, with_retry
req = ForgetBuilder(memory_id).build() # build once → stable request_id
res = with_retry(lambda: client.forget(req))use std::time::Duration;
use brain_db_sdk::ForgetBuilder;
let req = ForgetBuilder::new(memory_id).build(); // build once → stable request_id
let mut attempt = 1u32;
loop {
match client.forget(&req).await {
Ok(_) => break,
Err(e) => {
if !e.is_retryable() || attempt >= 3 {
return Err(e.into());
}
tokio::time::sleep(Duration::from_millis(100 * 2u64.pow(attempt - 1))).await;
attempt += 1;
}
}
}The SDK also ships a with_retry combinator that wraps this loop with jittered
backoff — see Errors.
Neither client takes an external AbortSignal. A call is bounded by its configured timeout, which fires an internal abort on expiry.
// HTTP: per-request timeout; expiry surfaces as BrainHttpError { status: 0 }.
const brain = new BrainHttpClient({ apiKey: "…", timeoutMs: 5_000 });
// Wire: connect + per-response read deadlines; a miss rejects with BrainTimeout.
const client = await BrainClient.connect("127.0.0.1", 9090, {
auth: { kind: "token", token: tokenBytes },
requestTimeoutMs: 5_000,
});# HTTP: per-request timeout in seconds.
brain = BrainHttpClient(os.environ["BRAIN_API_KEY"], timeout=5.0)
# Wire: configure via ClientConfig for connect_with; a miss raises BrainTimeout.use std::net::SocketAddr;
use std::time::Duration;
use brain_db_sdk::{Auth, BrainClient, BrainHttpClient, ClientConfig};
// HTTP: per-request timeout; expiry surfaces as a transport BrainHttpError (status 0).
let brain = BrainHttpClient::localhost("…").with_timeout(Duration::from_secs(5));
// Wire: connect + per-response read deadlines; a miss fails with BrainError::Timeout.
let addr: SocketAddr = "127.0.0.1:9090".parse()?;
let mut config = ClientConfig::new(Auth::Token(token_bytes));
config.request_timeout = Some(Duration::from_secs(5));
let client = BrainClient::connect_with(addr, config).await?;BrainTimeout is classified retryable by isRetryable. To cancel work in flight beyond the deadline, close the client — that tears down the connection.
Budgeting total time
A retrying call's worst case is roughly maxAttempts × timeout + accumulated backoff. If a route handler has a 30 s deadline, don't pair a 30 s per-attempt timeout with 5 retries — the retry loop alone can outrun the handler. Tighten the per-attempt timeout and cap maxAttempts so the product fits your budget.
A transport drop is reported retryable, but the wire client does not transparently reconnect a dead socket — a retry over a closed connection re-fails fast. Recover by opening a fresh BrainClient.connect(...).
Don't rebuild a wire request inside the retry loop. A fresh build() mints a new requestId, so the server treats the retry as a new logical write instead of deduplicating it.
Was this page helpful?
Error handling
Catch the right error per client — BrainHttpError for HTTP, the BrainError hierarchy for the wire — decide retry vs surface, and know which of the nine categories are transient.
Identity walkthrough
How an API key carries identity — it binds a (namespace, agent) scope at creation, the server derives everything from it, and you confirm it with whoami.