Persistence Backends
Generic key-value storage via AsyncPersistStore. Persistence backends are the foundational storage layer — durability, context, and other protocols compose on top of them. They store arbitrary JSON-serializable data indexed by string keys, with namespace isolation and prefix-based listing.
What Persistence Backends Do
Persistence backends provide six core operations:
save(key, value)— Store a JSON-serializable dict, overwriting if the key exists (upsert semantics).load(key)— Retrieve a value by key, orNoneif not found.delete(key)— Remove a key, returningTrueif it existed.exists(key)— Check key existence without loading the value.list_keys(prefix)— List keys, optionally filtered by prefix (enables namespace listing).clear()— Delete all keys in the namespace.
The interface is intentionally minimal — it’s a key-value store, not a document database. Higher-level backends (durability, memory) build domain-specific semantics on top of these primitives.
Namespace pattern: Keys use prefixes for logical separation (thread:abc123, memory:xyz789, config:default). The list_keys(prefix) operation enables efficient namespace enumeration.
Backend Comparison
| Feature | SQLitePersistStore | PostgreSQLPersistStore |
|---|---|---|
| Storage engine | SQLite (file-based) | PostgreSQL |
| JSON storage | Serialized JSON in TEXT column | Native JSONB |
| Concurrent writes | ❌ Single writer (asyncio.Lock) | ✅ Connection pool |
| Connection pooling | Reader pool (default 5, WAL mode) | ✅ (default 10 connections) |
| Namespace isolation | Key prefix | Key prefix + separate namespace column |
| Async I/O | asyncio.to_thread (non-blocking) |
psycopg async pool |
| Setup complexity | Low — zero external deps | Medium — requires PostgreSQL |
| Production readiness | ⚠️ Local/dev, single-process | ✅ Multi-process, scalable |
| Connection recovery | N/A (local file) | ✅ Auto-reset on recoverable errors |
Both implement the same interface identically. The choice is purely about the storage engine’s concurrency and operational characteristics.
Available Backends
SQLitePersistStore
Local key-value storage using SQLite with WAL mode and a reader pool.
Key characteristics:
- WAL mode enables concurrent reads alongside a single writer. The writer connection is serialized via
asyncio.Lock(SQLite connections are not thread-safe across workers). - Reader pool (default 5 connections) allows parallel reads across concurrent loops — critical for multi-loop scenarios.
- Namespace isolation via key prefixing, matching the PostgreSQL backend’s pattern.
- Lazy connection initialization — connections are created on first use, not at construction.
- All sync SQLite operations run via
asyncio.to_threadto avoid blocking the event loop (IG-258 Phase 2).
When to choose: Local development, single-process production, embedded deployments, or any scenario avoiding external database dependencies.
Minimal config:
protocols:
persistence:
enabled: true
backend: sqlite
persist_dir: ~/.soothe/persistence
Source: packages/soothe/src/soothe/backends/persistence/sqlite_store.py
PostgreSQLPersistStore
Production-grade key-value storage using PostgreSQL with JSONB and async connection pooling.
Key characteristics:
- Native JSONB storage — efficient JSON querying and indexing at the database level, no serialization overhead.
- Connection pool via
psycopg_pool.AsyncConnectionPool(default 10 connections, min 1). Supports concurrent multi-process access. - Automatic table creation with indexes on key and namespace columns.
- Connection recovery — detects transient PostgreSQL failures (
OperationalError,InterfaceError,AdminShutdown,CrashShutdown,ConnectionFailure) and automatically resets the pool. Non-recoverable errors propagate immediately. - Async-safe lazy initialization with
asyncio.Lockto prevent race conditions during pool creation.
When to choose: Multi-process production, horizontal scaling, or when PostgreSQL is already deployed for other Soothe backends (durability, vector store).
Minimal config:
protocols:
persistence:
enabled: true
backend: postgresql
dsn: postgresql://localhost/soothe
pool_size: 10
Source: packages/soothe/src/soothe/backends/persistence/postgres_store.py
Performance Characteristics
| Operation | SQLite | PostgreSQL | Notes |
|---|---|---|---|
save() |
~10-20ms | ~20-50ms | PG has pool + network overhead |
load() |
~5-10ms | ~10-30ms | SQLite is faster for local reads |
delete() |
~10-20ms | ~20-50ms | Single-writer on SQLite |
exists() |
~5-10ms | ~10-30ms | Fast check, no payload |
list_keys() |
~50-100ms | ~50-200ms | Index scan; PG adds network |
clear() |
~50-100ms | ~50-200ms | Bulk delete |
SQLite is faster for single-process workloads because there’s no network overhead. PostgreSQL wins under concurrent multi-process load where SQLite’s single-writer lock becomes a bottleneck.
Production Gotchas
-
SQLite writer serialization: Even with WAL mode, all writes go through a single connection guarded by
asyncio.Lock. Under write-heavy concurrent workloads, this creates contention. Monitor write latency under load. -
PostgreSQL pool sizing: The default pool size (10) matches the checkpointer pattern. For high-concurrency scenarios, increase
pool_size. Too few connections cause queueing; too many exhaust database resources. -
Connection failure handling:
PostgreSQLPersistStoreresets the pool on recoverable errors but propagates non-recoverable ones. Ensure your error handling accounts for both — aPoolResetmay leave the store temporarily without a pool during reset. -
Key naming conventions: Use consistent prefix patterns (
namespace:key). Thelist_keys(prefix)operation relies onLIKE prefix%queries — inconsistent naming defeats namespace isolation. -
No native transactions across keys: Each
save()/delete()is an independent transaction. Multi-key atomic operations require application-level coordination. -
SQLite database file growth: WAL mode creates
-waland-shmsidecar files. These are normal and are checkpointed automatically. Don’t delete them while the database is in use.
Integration Patterns
Persistence backends are the foundation other backends build on:
- Durability:
BasePersistStoreDurabilitydelegates all thread storage to anAsyncPersistStoreinstance.SQLiteDurabilitycreates aSQLitePersistStorewith namespace=durability;PostgreSQLDurabilityreceives a pre-built store. - General-purpose storage: Use for config, metadata, and arbitrary state —
save("config:default", config_dict),save("thread_state:abc123", state). - Namespace listing:
list_keys("thread:")returns all thread keys;list_keys("memory:")returns all memory keys.
Related Documentation
- VectorStoreProtocol & AsyncPersistStore — Protocol interface definition
- Backends Overview — Backend layer introduction
- Durability Backends — Composes on top of PersistStore
- Memory Backends — Memory storage patterns