Concurrency
Multi-process SQLite write safety in Wolbarg 0.4 — BEGIN IMMEDIATE, busy_timeout, exponential backoff, WOLBARG_STORAGE_LOCKED, and published multi-writer benchmarks.
What is it?
Wolbarg 0.4 hardens multi-process writers against a single SQLite memory.db — the topology most multi-agent setups use (one Node process or worker per agent, one shared file).
Before 0.4, SQLite mutating paths used deferred BEGIN. Under concurrent writers that commonly produced SQLITE_BUSY at commit time after work had already started. 0.4 acquires the write lock up front and retries with backoff when the lock is contended.
Guarantees
| Guarantee | Detail |
|---|---|
| WAL mode | Enabled by default so readers do not block writers |
BEGIN IMMEDIATE | Mutating transactions take the write lock before doing work |
| Busy timeout | SQLite busy_timeout pragma honors lockTimeoutMs |
| App-level retry | On SQLITE_BUSY, exponential backoff + jitter up to maxRetries |
| Stable error | Exhausted retries throw WOLBARG_STORAGE_LOCKED with an actionable suggestion |
| In-process mutex | Same-process concurrent writers are additionally serialized |
PostgreSQL is unchanged for this feature — it already uses row-level locking and connection pooling. Concurrency config is ignored on the Postgres provider.
Why it matters for agents
Multi-agent frameworks often open the same SQLite file from several processes:
- Parallel tool workers writing preferences / facts
- Dev servers + background jobs sharing local memory
- CLI agents launched side-by-side against one project DB
Without write-lock discipline, you get intermittent failures, orphaned retries, or corrupted mental models when agents silently drop writes. Wolbarg 0.4 makes contention visible, retryable, and bounded.
Configuration
import { wolbarg, openaiEmbedding } from "wolbarg";
const ctx = wolbarg({
organization: "my-org",
database: { provider: "sqlite", url: "./memory.db" },
embedding: openaiEmbedding({
apiKey: process.env.OPENAI_API_KEY!,
model: "text-embedding-3-small",
}),
concurrency: {
maxRetries: 5, // default — app-level retry attempts after SQLITE_BUSY
baseBackoffMs: 50, // default — first backoff floor
maxBackoffMs: 2000, // default — backoff ceiling
lockTimeoutMs: 5000, // SQLite busy_timeout pragma (ms)
},
});Defaults are conservative for a handful of concurrent agent processes. Raise maxRetries / lockTimeoutMs for bursty writers; switch to Postgres when you need high fan-out multi-tenant write throughput.
Defaults
| Field | Default | Meaning |
|---|---|---|
maxRetries | 5 | How many times Wolbarg retries a busy transaction |
baseBackoffMs | 50 | Starting delay before retry (grows exponentially) |
maxBackoffMs | 2000 | Cap on backoff delay |
lockTimeoutMs | 5000 | Passed to SQLite busy handler / busy_timeout |
How it works (internals)
- A mutating path (
remember,update,forget, ingest writes, compress archive, …) opens a transaction withBEGIN IMMEDIATE. - If another process holds the write lock, SQLite returns busy within
lockTimeoutMs. - Wolbarg sleeps with exponential backoff + jitter and retries up to
maxRetries. - If all attempts fail, it throws
WOLBARG_STORAGE_LOCKED— not a generic SQLite string. - Same-process callers also queue through an in-process mutex so Node async concurrency does not pile on top of OS-level lock storms.
Tradeoffs
| Upside | Cost |
|---|---|
Far fewer surprise SQLITE_BUSY failures | p95/p99 latency rises under contention |
| Bounded, actionable errors | Very high writer fan-out still serializes on one file |
| Works with existing local-first topologies | Postgres remains better for SaaS-scale concurrent writes |
Tune for your workload, or move shared multi-tenant agents to the PostgreSQL backend.
Errors
| Code | When | What to do |
|---|---|---|
WOLBARG_STORAGE_LOCKED | Write-lock retries exhausted | Increase maxRetries / lockTimeoutMs, reduce writer fan-out, or switch to Postgres |
Suggestion text on the error points at the same remediation.
Benchmarks
Same-process stress (v4 suite)
Published dual-backend v0.4 stress numbers (SQLite · Postgres):
| Writers | SQLite throughput | SQLite p95 | Postgres throughput | Postgres p95 |
|---|---|---|---|---|
| 8 × 20 ops | 6,084 ops/s | 3.05 ms | 2,555 ops/s | 8.51 ms |
| 16 × 20 ops | 8,660 ops/s | 2.46 ms | 3,335 ops/s | 9.48 ms |
| 32 × 20 ops | 6,798 ops/s | 22.94 ms | 3,802 ops/s | 14.77 ms |
Mock embeddings; Node v24 · win32/arm64 · 8 CPUs · generated 2026-07-18. Absolute ops/sec vary by machine.
Multi-process levels
True multi-process writers (separate OS processes, shared SQLite file):
npx tsx benchmark/multiprocess-levels.tsArtifact: /benchmarks/multiprocess-concurrency.json — levels 2 / 5 / 10 / 20 writers with throughput, p50/p95/p99, error rate, and integrity checks. At 20 writers this run held 0% error rate with integrity OK (p95 rises as expected under serialization).
Interactive charts: /benchmarks. Methodology: Benchmarks.
When to use Postgres instead
Choose PostgreSQL when:
- Many agents share one cluster across hosts
- You need cross-process real-time events (
LISTEN/NOTIFY) - Write fan-out regularly exceeds what a single SQLite file can serialize comfortably