@takk/bayesretry
v1.0.0
Published
BayesRetry: adaptive retry policies that decide per call whether to retry an external request, holding a Beta posterior over the probability of success given the endpoint and the attempt number and retrying only when the cost-aware expected value of tryin
Maintainers
Readme
BayesRetry
Universal, zero-runtime-dependency NPM library and CLI for adaptive, cost-aware retry policies that decide per call whether retrying is worth it, for Massive Intelligence (IM) systems.
BayesRetry sits between your application and the requests it depends on. After a call fails, it holds a Beta posterior over the probability that another attempt will succeed, conditioned on the endpoint, the attempt number, and the class of error that just occurred. Each outcome is one Bernoulli trial, folded in with the closed-form Beta-Bernoulli conjugate update. It retries only when the cost-aware expected value of trying again, the success probability times the value of a success minus the cost of an attempt, is positive. Not because three is the magic number.
Core promise: zero required runtime dependencies, single-function setup, exact closed-form inference with O(1) updates and zero inference latency, a node-free core that runs in Node, edge runtimes, and the browser, ESM plus CJS dual distribution, and SLSA provenance on every release.
Why BayesRetry
A retry loop is a decision under uncertainty: will trying again succeed, or just waste a call and add load to a service that is already struggling. Today that decision runs on a constant: retry three times, back off, give up. That constant is wrong in both directions. It hammers a hard-down endpoint with three useless attempts, and it abandons a transient one that would have recovered on the fourth. BayesRetry replaces the constant with the correct primitive: an explicit prior, continuous updates from evidence, and a cost-aware expected-value decision you can act on and audit.
What sets it apart from a fixed retry count:
- Learns per endpoint and per error class. A 429 rate limit almost always clears on retry, a 401 never does, and a 503 sometimes does. BayesRetry conditions its belief on the error class, so it never averages a recoverable rate limit and a broken deploy into one misleading number.
- Decides on expected value, not a count. It retries only when
probability * successValue - attemptCostis positive, so an expensive call to a flaky provider is held to a higher bar than a cheap one. - Exact, not approximate. The credible interval the circuit breaker reads is the true Beta quantile, computed from
lgammavia the Lanczos approximation, the regularized incomplete beta via Lentz's continued fraction, and its inverse via bisection. - Breaks the circuit on evidence. The circuit breaker opens an endpoint when the upper bound of the credible interval on its success rate falls below a floor, not after an arbitrary count of consecutive failures.
- Caps the blast radius. A per-window retry budget stops one agent from piling unbounded retries onto a degraded dependency, even when each individual retry still looks worth it. Aggregate it across a fleet through the observer hook and a shared store of your own.
- Honors the server. The fetch and axios adapters read a
Retry-Afterheader and wait at least as long as the server asked, over the policy's own backoff. - Proves what happened. A tamper-evident SHA-256 hash-chained audit trail of decisions you can seal and verify, the evidence a compliance review asks for when retry spend is questioned.
Install
pnpm add @takk/bayesretry
# or: npm install @takk/bayesretry
# or: yarn add @takk/bayesretry
# or: bun add @takk/bayesretryThe core has zero required runtime dependencies. Every @takk sibling is an optional peer; install only what you compose with.
Quickstart
// src/example.ts
import { createRetryPolicy } from '@takk/bayesretry';
const policy = createRetryPolicy();
// One call, retried adaptively. The policy learns from every outcome and
// retries only while trying again is worth more than it costs.
const data = await policy.run('search-api', async (attempt) => {
const response = await fetch('https://api.example.com/search');
if (!response.ok) throw Object.assign(new Error(String(response.status)), { status: response.status });
return response.json();
});A brand-new endpoint starts at the uniform prior Beta(1, 1). The decision moves with the evidence, and the policy stops as soon as the expected value of another attempt turns negative.
Quickstart, wrap your fetch
The fastest way to adopt BayesRetry is to wrap the client you already use. wrapFetch returns a drop-in fetch that retries adaptively and honors a server Retry-After header over its own backoff.
import { createRetryPolicy } from '@takk/bayesretry';
import { wrapFetch } from '@takk/bayesretry/http';
const policy = createRetryPolicy();
const fetch = wrapFetch(policy);
// Use it exactly like fetch. A 503 is retried on the evidence; a 401 is not.
const response = await fetch('https://api.example.com/v1/chat');attachAxiosRetry(policy, axiosInstance) does the same for an axios instance through a response interceptor, with axios kept as an optional peer that is never imported.
Entry points
Thirteen subpath exports, each importable on its own. The core is node-free; only node touches a Node built-in.
| Import | What it gives you |
|---|---|
| @takk/bayesretry | The createRetryPolicy facade wiring everything below, plus the full toolkit. |
| @takk/bayesretry/beta | The Beta distribution: priors, moments, cdf, quantile, credible interval, conjugate update, sampling. |
| @takk/bayesretry/store | The PosteriorStore keyed by (endpoint, attempt, error class), and portable JSON snapshots. |
| @takk/bayesretry/decision | The DecisionEngine: the expected-value retry rule and Thompson exploration. |
| @takk/bayesretry/cost | The CostModel and expectedValueOfRetry. |
| @takk/bayesretry/breaker | The posterior-calibrated CircuitBreaker. |
| @takk/bayesretry/budget | The RetryBudget, a fixed-window cap on aggregate retry cost. |
| @takk/bayesretry/decay | Exponential time-decay of stale evidence back toward the prior. |
| @takk/bayesretry/backoff | computeBackoff: exponential backoff with full, equal, or no jitter. |
| @takk/bayesretry/http | wrapFetch, attachAxiosRetry, parseRetryAfter, isRetryableStatus. |
| @takk/bayesretry/audit | Append-only SHA-256 hash-chained audit log of decisions, via Web Crypto. |
| @takk/bayesretry/node | createFileStore, durable file-backed persistence with atomic writes. |
| @takk/bayesretry/edge | The node-free core, re-exported for edge runtimes and the browser. |
Error-class aware beliefs
The same endpoint can fail in ways that mean very different things. BayesRetry keeps them apart.
import { createRetryPolicy } from '@takk/bayesretry';
const policy = createRetryPolicy();
// Teach the same endpoint two histories: 429 recovers, 500 does not.
for (let i = 0; i < 40; i += 1) {
policy.observe({ endpoint: 'provider', attempt: 1, errorType: '429' }, true);
policy.observe({ endpoint: 'provider', attempt: 1, errorType: '500' }, false);
}
policy.shouldRetry({ endpoint: 'provider', attempt: 1, errorType: '429' }).retry; // true
policy.shouldRetry({ endpoint: 'provider', attempt: 1, errorType: '500' }).retry; // falsePooling 429 and 500 into one rate would hide that retrying a rate limit pays and retrying a broken deploy does not. Conditioning on the error class is the difference.
Circuit breaker, calibrated by the posterior
const policy = createRetryPolicy({
breaker: { openBelow: 0.2, confidence: 0.95, minStrength: 8, cooldownMs: 30000 },
});The breaker opens an endpoint only when it is statistically confident the endpoint is failing: the upper bound of the credible interval on the success rate drops below openBelow, given at least minStrength of evidence. A brief wobble does not trip it; a sustained outage does. After the cooldown it half-opens to admit one probe, and the probe's outcome closes or reopens it. It is on by default; pass breaker: false to disable.
Retry budget, capping the blast radius
const policy = createRetryPolicy({
budget: { maxCostPerWindow: 20, windowMs: 60_000 },
});Even when each retry looks worth it, a runaway loop can amplify an outage. The budget caps the aggregate retry cost per window for this policy instance (one process); once it is spent, shouldRetry returns false with reason budget-exhausted until the window rolls over. For fleet-wide coordination, aggregate spend across processes through the observer hook into a shared store of your own. Disabled by default.
Durable persistence
The default store lives in memory. For learning that survives restarts with no database, use the file-backed store.
import { createRetryPolicy } from '@takk/bayesretry';
import { createFileStore } from '@takk/bayesretry/node';
const policy = createRetryPolicy();
const store = createFileStore('./retry-store.json');
policy.load(store.load());
// ... run requests ...
store.save(policy.snapshot()); // written atomicallyOn an edge runtime, import @takk/bayesretry/edge, snapshot to your own KV store between invocations, and learning survives a cold start with no Node dependency.
Tamper-evident audit trail
import { AuditLog } from '@takk/bayesretry/audit';
const log = new AuditLog();
await log.append({
endpoint: 'payments-api', attempt: 1, retried: true,
probability: 0.82, expectedValue: 0.72, reason: 'positive-expected-value', at: Date.now(),
});
await log.verify(); // true, until any entry is alteredEach decision is recorded append-only and chained to the previous one through a SHA-256 hash of its canonical form. Any later edit, deletion, or reordering breaks the chain and verify returns false. The seal is an integrity seal, not a digital signature: it proves the record was not tampered with, not who produced it. The chain uses the Web Crypto API, so the audit surface runs in Node, edge runtimes, and the browser.
Observability and governance
Every decision is observable. Pass an observer and the policy notifies you of each decision and each outcome, so you can emit an OpenTelemetry span per retry, ship the decision to a compliance log, or aggregate retry spend across a fleet in a shared store of your own. The policy never lets an observer error break a retry, so it is safe to wire production telemetry through it.
import { createRetryPolicy } from '@takk/bayesretry';
const policy = createRetryPolicy({
observer: {
onDecision: (decision, context) => {
span.addEvent('retry.decision', {
endpoint: context.endpoint,
attempt: context.attempt,
errorType: context.errorType ?? 'default',
retry: decision.retry,
reason: decision.reason, // why: positive-expected-value, circuit-open, budget-exhausted, ...
expectedValue: decision.expectedValue,
});
},
onObserve: (context, success) => metrics.record(context.endpoint, success),
},
});Together with the tamper-evident audit trail, the observer is the governance seam for Massive Intelligence (IM) systems: every retry a non-human entity makes is explainable after the fact, with the reason, the probability, and the expected value it was decided on. No runtime dependency is taken; you wire it to your own telemetry stack.
CLI
BayesRetry ships a command-line tool that runs the real policy, with every number produced by execution.
# Simulate a flaky endpoint that suffers an outage and recovers, comparing the
# adaptive policy to a fixed three-retries baseline.
npx @takk/bayesretry simulate --n 500 --seed 1
# Replay a sequence of outcomes (1 success, 0 failure) and print the learned
# posterior and the decision for the next attempt.
echo "1 1 0 1 0 0 0" | npx @takk/bayesretry replayExit codes follow the sysexits convention: 0 ok, 64 usage, 65 data error, 66 missing input. See examples/ for runnable demos and benchmarks/ for the paired-stream benchmark.
The math, in one paragraph
Each context, an endpoint, an attempt number, and an error class, has a prior Beta(alpha, beta) over the probability that an attempt succeeds. An outcome y in {0, 1} updates the posterior to Beta(alpha + successes, beta + failures). The success probability used for a decision is the posterior mean (greedy) or a single posterior draw (Thompson, which explores). The expected value of one more attempt is probability * successValue - attemptCost, and the policy retries only when that value clears the floor, subject to a maxAttempts safety bound. The circuit breaker reads the exact Beta quantile of the aggregate posterior. Everything is closed form, O(1) per update, with zero inference latency. Priors are explicit and customizable per endpoint; the cost model is always yours, the calibration is always the library's.
Benchmark
benchmarks/retry-benchmark.mjs runs the adaptive policy and a fixed three-retries baseline over the same 6000-call stream across six endpoints with different recovery profiles, with both policies facing identical luck so the only variable is the decision rule. On the committed seed:
| Policy | Success rate | Attempts | Wasted retries | |---|---|---|---| | fixed three retries | 69.4% | 13350 | 5502 | | adaptive (mean) | 67.5% | 7697 | 72 |
The adaptive policy spends 42% fewer attempts and 99% fewer wasted retries while landing within two points of the baseline success rate, because it declines the rare, expensive successes on near-dead endpoints whose expected value does not cover their cost. Run it yourself: pnpm run build && node benchmarks/retry-benchmark.mjs.
Quality
- 68 tests across 20 suites, all passing under Vitest 4.
- Coverage: statements 93.51%, branches 86.46%, functions 94.21%, lines 95.87%.
- Lint clean under Biome 2.5.
- Typecheck clean under TypeScript 6 in maximum strict mode (
exactOptionalPropertyTypes,useUnknownInCatchVariables,noUncheckedIndexedAccess). publintclean and@arethetypeswrong/cligreen across all thirteen subpaths.size-limitunder budget on every bundle (brotli core 3.99 kB against a 6 kB limit).- Distribution smoke test exercising the compiled ESM and CJS artifacts and the compiled CLI spawned as a single Node process.
- Published with
--provenance(SLSA attestation by GitHub Actions).
See SPEC.md for the formal specification, public surface, and stability promise.
FAQ
Why not just retry three times with exponential backoff? That is a constant that ignores everything you have learned. It wastes three attempts on a hard-down endpoint and gives up on a transient one that would have recovered. BayesRetry decides per call on the evidence: it stops early on what is dead and keeps trying what recovers, and it proves the decision in an audit trail.
Is this not just a circuit breaker? A circuit breaker is one part of it, and a better one: this breaker opens on the credible bound of the success rate, not on a raw failure count. But the core is the per-call expected-value decision, conditioned on the error class, with a cost model and a retry budget on top. The breaker is the coarse, all-or-nothing guard; the posterior is the fine-grained one.
Does this work in Cloudflare Workers, Vercel Edge, Bun, and Deno?
Yes. The core is node-free; the audit seal uses the Web Crypto API, not node:crypto. Import @takk/bayesretry or @takk/bayesretry/edge anywhere with fetch and Web Crypto. Only @takk/bayesretry/node requires Node.
What is the cost model in, dollars or tokens?
Whatever unit you choose. successValue and attemptCost are in the same unit, and only their ratio matters to the decision. Set them in dollars, tokens, latency milliseconds, or any utility, globally or per endpoint.
Where does the state live?
By default, in-process memory, with portable JSON snapshots. For durability, use createFileStore from @takk/bayesretry/node. For an edge runtime, snapshot to your own KV store. For multi-process coordination, share a snapshot or implement the PosteriorStore interface over your database.
Contributing
See .github/CONTRIBUTING.md for the contributor guide. Substantive proposals open a GitHub Issue first; trivial fixes can go straight to a PR. All commits require DCO sign-off (git commit -s). Non-trivial contributions are governed by the Contributor License Agreement.
Community and support
- Issues and feature requests. Open a GitHub issue at
davccavalcante/bayesretry/issues. Include the package version, a minimal reproduction, expected versus actual behavior, and where relevant theshouldRetry()decision orrank()output. - Security disclosures. Do not open public issues for vulnerabilities. Follow the responsible-disclosure flow in
SECURITY.md, contact[email protected](or[email protected]) with the[SECURITY]prefix. - Code of Conduct. This project follows the Contributor Covenant 2.1. Participation in any BayesRetry space implies agreement.
- Contributions. All non-trivial contributions go through the Contributor License Agreement. Tests, lint, typecheck, and build must be green before review (
pnpm verify).
Author
Created by David C Cavalcante, [email protected] (preferred), [email protected] (Takk relay), linkedin.com/in/hellodav, x.com/davccavalcante, takk.ag.
BayesRetry is part of a broader portfolio of NPM packages targeting Massive Intelligence (IM) native infrastructure for 2026-2030, built at Takk Innovate Studio.
Related research by the author
The architectural philosophy behind BayesRetry, separating inference, decision, and persistence into composable, independently-governed layers, echoes the author's research frameworks:
- MAIC (Massive Artificial Intelligence Consciousness), a systemic intelligence framework designed to coordinate, supervise, and govern large-scale Massive Intelligence ecosystems, providing global context awareness, alignment, and orchestration across multiple models, agents, and decision layers.
- HIM (Hybrid Entity Intelligence Model), a hybrid intelligence layer that integrates Massive Intelligence systems with human-defined logic, rules, heuristics, and strategic intent, interpreting objectives and structuring decision-making before and after model execution.
- NHE (Noumenal Higher-order Entity), a non-human cognitive entity with a defined functional identity and operational agency within a Massive Intelligence ecosystem, operating through coordinated intelligence layers while maintaining a non-anthropomorphic identity.
These frameworks are published independently of BayesRetry and are separate works:
- Research papers: The Soul of the Machine, Beyond Consciousness in LLMs, The Cave of Silence.
- PhilPapers profile: David Cortes Cavalcante.
- Hugging Face: TeleologyHI.
- GitHub: davccavalcante, Takk8IS.
Sponsors
Join the journey as the portfolio continues to ship Massive Intelligence (IM) native infrastructure. Your support is the cornerstone of this work.
- Sponsor on GitHub: github.com/sponsors/davccavalcante
- USDT (TRC-20):
TS1vuhMAhFpbd7y68cu5ZtP9PsXVmZWmeh
Privacy
BayesRetry runs entirely inside your own process and infrastructure. It makes no outbound calls to the author, collects no telemetry, and ships no analytics. The only state it holds is the retry evidence you feed it. See PRIVACY.md for the full data-handling notice, including how the optional file store persists state on disk.
License
Licensed under the Apache License 2.0. See LICENSE for the full text and NOTICE for attribution and third-party component licenses. You may use, modify, and distribute the code under the terms of that license, including its patent grant and attribution requirements.
