Installation
BanditDB has two components: the Rust engine and a client SDK (Python or TypeScript/JavaScript). Start the engine first, then install the SDK.
1. Start the Engine
Binary (fastest, no Docker required)
curl -fsSL https://raw.githubusercontent.com/dynamicpricing-ai/banditdb/main/scripts/install.sh | sh
banditdb
curl http://localhost:8080/health # {"status":"ok"}
Requires glibc >= 2.39 (Ubuntu 24.04+ or equivalent). On older distros — e.g. Ubuntu 22.04 (glibc 2.35) — the binary won't run. Use Docker or Build from Source below instead.
Docker
docker run -d -p 8080:8080 \
-e BANDITDB_API_KEYS=admin-key=admin \
-v banditdb_data:/data \
simeonlukov/banditdb:latest
Docker Compose (recommended for production)
Includes persistence, RBAC, and auto-checkpointing:
curl -fsSL https://raw.githubusercontent.com/dynamicpricing-ai/banditdb/main/docker-compose.yml -o docker-compose.yml
# Set BANDITDB_API_KEYS=admin-key=admin;app-key=writer in .env
docker compose up -d
Key environment variables:
| Variable | Required | Default | Description |
|---|---|---|---|
BANDITDB_API_KEYS | Production | — | Multi-key RBAC: key1=admin;key2=writer;key3=reader. Without it the server runs in open mode — every caller is granted admin. Fine for local dev, never for production. |
BANDITDB_REQUIRE_AUTH | Production | false | Set true so a missing key set is fatal at startup instead of silently opening the database. A mistyped secret then stops the pod rather than publishing an unauthenticated instance. Enabled by default in the Helm chart. |
BANDITDB_CORS_ORIGINS | Optional | deny all | Comma-separated browser origins allowed to call the API. Empty denies all cross-origin requests; * permits any — avoid that if keys ever reach client-side code. Before 2.0.0 any origin was allowed. |
BANDITDB_METRICS_PUBLIC | Optional | false | /metrics requires a reader key by default, since its output names campaigns and arms. Set true to restore anonymous access for a scraper that cannot send headers. |
BANDITDB_TENANT_MODE | Optional | false | Set true to enable multi-tenancy. Requires tenant-scoped keys in BANDITDB_API_KEYS: key=role:tenant_id, e.g. acme-key=writer:acme;globex-key=writer:globex. Each tenant's campaigns are namespaced and isolated — a key can only see/write its own tenant's data. |
BANDITDB_CHECKPOINT_INTERVAL | Optional | 5000 | Auto-checkpoint after N rewarded events |
BANDITDB_MAX_WAL_SIZE_MB | Optional | 100 | Also checkpoint when WAL exceeds N MB |
DATA_DIR | Optional | /data | WAL, checkpoint, and Parquet export directory. Set it to a persistent volume — the default in a plain binary run is the current directory. |
PORT | Optional | 8080 | HTTP listen port |
LOG_FORMAT | Optional | unset | Log output format. Unset (default): human-readable text, best for terminals and docker logs. json: one structured JSON object per line — for CloudWatch, Datadog, Splunk, Loki and other log aggregators. Any other value falls back to text. |
RUST_LOG | Optional | info | Log verbosity filter. Examples: debug (everything), warn (quiet), banditdb=debug,tower_http=warn (per-module). Standard Rust EnvFilter syntax. |
BANDITDB_RATE_LIMIT_PER_SEC | Optional | 1000 | Per-key request rate limit. Raise it for load tests and bulk backfill — the default will quietly fail a tight ingest loop with 429s. |
BANDITDB_MAX_PENDING_INTERACTIONS | Optional | 100000 | Ceiling on predictions awaiting a reward, roughly 1 KB each. TTL alone does not bound this: at 1,000 predictions/second the 24-hour default would hold 86 million records. Eviction permanently breaks reward matching for that prediction, so alert on banditdb_interactions_evicted_total. |
BANDITDB_MAX_CONTEXT_MAGNITUDE | Optional | 1e6 | Largest absolute value accepted in a context vector. Guards against values that overflow the rank-one update and poison the campaign with NaN. |
BANDITDB_EXPORT_RETAIN_SHARDS | Optional | 50 | Parquet shards kept per campaign. 0 keeps everything, which grows until the volume fills and checkpointing starts failing. |
BANDITDB_FSYNC_INTERVAL_MS | Optional | 200 | Group-commit window. Measured to make almost no difference across a 20× range — the writer also syncs whenever it goes idle — so this is not a useful tuning lever. Leave it. |
Kubernetes (Helm)
helm install banditdb ./helm/banditdb \
--set auth.apiKeys="admin-key=admin;app-key=writer" \
--set persistence.size=10Gi \
--set ingress.enabled=true \
--set ingress.host=banditdb.example.com
The Helm chart includes a PersistentVolumeClaim, Secret for API keys, liveness/readiness probes, and a Recreate deployment strategy — required for single-writer WAL safety.
Build from source
Requires Rust 1.97 or newer.
git clone https://github.com/dynamicpricing-ai/banditdb
cd banditdb
cargo build --release # standard build
# cargo build --release --features neural # with NeuralLinUCB
./target/release/banditdb
One process per data directory. BanditDB takes an exclusive
lock on DATA_DIR; a second process on the same volume refuses to
start. Two writers would interleave WAL appends and corrupt both copies with no
error at the time, so this is enforced rather than documented. It is also why
the Helm chart pins replicaCount: 1 with the Recreate
strategy.
2. Install the SDK
Python
pip install banditdb-python
from banditdb import Client
db = Client("http://localhost:8080", api_key="your-secret-key")
Smallest possible working loop — create a campaign, ask for a decision, report the outcome:
# One campaign: which button color converts best?
db.create_campaign("button-color", arms=["red", "green", "blue"], feature_dim=2)
# Ask for a decision. Context: [is_mobile, is_returning_visitor]
arm, interaction_id = db.predict("button-color", [1.0, 0.0])
print(arm) # e.g. "green"
# Report what happened: 1.0 = clicked, 0.0 = ignored
db.reward(interaction_id, 1.0)
Normalise your context if the features are on different scales. It matters more than it looks: the exploration term scales with the vector's magnitude, and unnormalised input still converges — just far more slowly, with no error to tell you. On one benchmark this single change cut cumulative regret from 2,026 to 709.
from banditdb import normalize_context
ctx = normalize_context([age, income, sessions]) # unit L2 norm
arm, iid = db.predict("button-color", ctx)
SDK 0.2.0 requires server 2.0.0 or newer. It also validates locally before
sending, so a bad context raises ValueError naming the offending
index rather than costing a round trip.
That is the whole API surface you need day to day. See Quick Start for a realistic end-to-end example.
TypeScript / JavaScript
npm install banditdb-js
import { BanditDBClient } from "banditdb-js";
const db = new BanditDBClient({ url: "http://localhost:8080", apiKey: "your-secret-key" });
await db.createCampaign("button-color", { arms: ["red", "green", "blue"], feature_dim: 2 });
const { arm_id, interaction_id } = await db.predict("button-color", [1.0, 0.0]);
console.log(arm_id); // e.g. "green"
await db.reward(interaction_id, 1.0);
HTTP (no SDK)
Every SDK call is a plain JSON HTTP request. Use X-Api-Key header when auth is enabled:
curl -s http://localhost:8080/health
# {"status":"ok",...}
curl -s -X POST http://localhost:8080/predict \
-H "Content-Type: application/json" \
-H "X-Api-Key: your-secret-key" \
-d '{"campaign_id":"my-campaign","context":[1.0,0.5,0.3]}'