Brainby arc-labs/docs
Guide

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.

Prerequisites
  • A Brain client.
  • Familiarity with Error handling.
  • A latency budget you want calls to respect.
What you'll build

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?

On this page