How Recovery Works
BanditDB survives crashes and restarts automatically. No manual intervention required.
The Tuple Lifecycle
Every decision moves through five stages, from predict() to a folded-in matrix update:
(ctx, arm,
propensity)
observes
outcome
+ reward
join by interaction_id
state mutates
ยท self-healing retries
O(dยฒ) in-place
ยท ยตs
closed tuples โ Parquet export ยท replayable ยท causal analysis via logged propensities (LinUCB)
The Two Files
| File | Purpose |
|---|---|
checkpoint.json | Snapshot of all campaign matrices (Aโปยน, b, ฮธ, counts), plus the predictions still awaiting a reward. |
checkpoint.prev | The previous generation, retained. If the current checkpoint is unreadable this is what recovery falls back to. |
bandit_wal.jsonl | Append-only event log: CampaignCreated, Predicted, Rewarded, CampaignDeleted. |
Phase 1 โ Load the Checkpoint
If checkpoint.json exists, BanditDB reads it and restores all campaign matrices directly into memory โ no replaying, just deserialisation. The checkpoint records the WAL byte offset at which it was taken.
If no checkpoint exists, BanditDB starts from an empty state and replays the entire WAL from byte 0.
If a checkpoint exists but cannot be read, BanditDB falls back to
checkpoint.prev. If neither is readable it refuses to
start rather than coming up empty โ an empty start would serve a blank
database and then overwrite the evidence at the next checkpoint. Set
BANDITDB_ALLOW_CORRUPT_CHECKPOINT=true to override and accept the
loss.
Phase 2 โ Replay the WAL Tail
BanditDB opens bandit_wal.jsonl, seeks to the checkpoint's byte
offset, and replays every event written after that point. A completed checkpoint
ends with WAL rotation, which rewrites the log to begin exactly at the checkpoint
boundary โ so the recorded offset is 0 and replay starts from the beginning of
the rotated file.
Durability Guarantees
Durability is split by event type, deliberately. What you get depends on what you wrote.
| Event | Guarantee |
|---|---|
| Rewards | Acknowledged only after fsync.
POST /reward does not return until the record is on disk, so a
200 response survives process death, power loss, and VM preemption alike. |
| Campaign lifecycle create, delete, archive, restore |
Same guarantee. A 200 from POST /campaign means the campaign
survives a restart. |
| Predictions | Best-effort. Under WAL backpressure a prediction log record is
dropped rather than failing the request. Watch
banditdb_wal_dropped_total. |
RPO for acknowledged writes is zero. This is not bounded by the checkpoint interval โ checkpointing controls WAL size and replay time, not durability. Only in-flight requests that never received a response are lost, which is ordinary for any database.
Predictions are weaker on purpose, and it costs less than it sounds. A lost
prediction record costs the ability to match one late reward; it never
affects model state. The predictions still awaiting a reward travel inside
checkpoint.json, so a reward arriving after a restart still matches
the prediction that preceded it.
The cost: waiting for the fsync adds roughly 3.4 ms to a reward call at concurrency 1. The server amortises one fsync across all concurrent callers, so parallel submission reaches ~4,400 rewards/second while a strictly serial loop pays the full latency every time. Batch or thread your reward submission if throughput matters.
What POST /checkpoint Does
- Flush barrier โ drains all pending events and
fsyncs to disk, responds with confirmed byte offset. - Snapshot โ serialises all campaign matrices and the predictions still awaiting a reward to
checkpoint.tmp. The tmp file isfsynced, the previous generation is retained ascheckpoint.prev, then the rename is made durable byfsyncing the directory. - Parquet export โ joins
Predicted+Rewardedevents, writes matched pairs as a timestamped shard per campaign. Shards older thanBANDITDB_EXPORT_RETAIN_SHARDS(default 50) are pruned. - Neural retrain (NeuralLinUCB campaigns only) โ if
retrain_everyrewards have accumulated since the last retrain, runs Algorithm 2, re-accumulates arm matrices in the new embedding space, and saves.safetensorsweights to{DATA_DIR}/neural/. - WAL rotation โ truncates WAL to only the tail. Pre-checkpoint history is no longer needed for recovery.
Parquet files are analytics exports only โ not used for recovery. Losing them does not affect model state. Recovery uses only checkpoint.json + bandit_wal.jsonl.
Recommended Production Setup
# Auto-checkpoint every 10,000 rewards
BANDITDB_CHECKPOINT_INTERVAL=10000
# Or cap WAL size (useful on edge deployments)
BANDITDB_MAX_WAL_SIZE_MB=50
# Back up the two recovery files on a schedule
cp /data/checkpoint.json /backup/checkpoint-$(date +%s).json
cp /data/bandit_wal.jsonl /backup/wal-$(date +%s).jsonl
To move BanditDB to a new host: copy checkpoint.json and bandit_wal.jsonl to the same DATA_DIR on the new machine and start. Recovery is automatic.