Performance
MemDb runs with minimal configuration by default. Most optimizations — indexes, cloning, flush interval — can be introduced or changed at any time.
Tuning checklist
Each lever targets a specific concern; the right combination depends on workload.
| Lever | What it affects |
|---|---|
| Custom cloner | Every cache-boundary crossing — insert, update, find, query. Eliminates serialization from the caller-observed path entirely. |
| Custom serializer | Cold start; on-disk footprint; background flush throughput. Affects the caller path only when no custom cloner is registered (the default cloner round-trips through the serializer). |
| Identity index | By-id lookup, update, and delete — O(1) dictionary lookup instead of full scan. No effect on predicate-based operations. |
| Applied indexes | Queries on indexed properties — O(1) or O(log n) instead of full scan. Adds maintenance cost on every insert, update, and delete. |
| Flush interval | Data-loss window and background I/O frequency. Never affects caller-observed throughput — operations complete as soon as the cache is updated and the change is enqueued. |
PurgeCache | RAM held by stale and deleted records in long-running instances. |
| Partitioning | Cache-lock contention — each dataset has its own lock, indexes, and persister. |
Storage
Use an SSD. Spinning disks degrade write performance materially because the persister appends sequentially on every flush — read latency is RAM-bound, but write latency is disk-bound.
Mode
Use AppendOnly for write-heavy, read-free scenarios. It bypasses the in-memory cache entirely and is the fastest write path. All reads, updates, and deletes throw in this mode; only inserts and unfiltered Count() are supported.
Flush interval
The default flush interval is 5 seconds. The flush timer governs how often the persister drains its insert and state-modification queues to disk on a background thread. It does not sit on the caller’s Insert, Update, or Delete path — those operations complete as soon as the record is cloned, the cache is updated, and the change is enqueued.
Adjusting the flush interval therefore does not change caller-observed insert or update throughput. What it does change:
- Background I/O frequency. A longer interval reduces the number of file open/seek/write cycles the persister performs and lowers syscall overhead.
- Queue memory pressure. Records pending a write accumulate in the persister’s queues between flushes; a longer interval allows those queues to grow larger.
- Data-loss window on abnormal exit. A longer interval widens the window of writes that exist only in the queue and have not yet reached disk.
- The cost of an explicit
db.Flush()or dispose. Larger queues take longer to drain when a flush is finally forced.
Increase the interval (up to a maximum of 60 seconds) for sustained high-write workloads where reduced I/O frequency is desirable and the wider data-loss window is acceptable. Decrease it — or call db.Flush() at critical checkpoints — for workloads that need tighter durability guarantees.
Setting the interval to 0 disables the timer entirely; writes are persisted only on explicit db.Flush() or on dispose.
Serialization
A custom binary IMemDbSerializer<T> can significantly improve throughput in performance-critical scenarios — both because binary encoding is denser than JSON and because the per-record allocation profile is lower.
Cloning
Provide a custom IMemDbCloner<T> to replace serialization-based deep cloning with a direct field copy. Cloning happens on every cache boundary crossing — insert, update, find, query, materialization — so the savings compound across read-heavy workloads.
Cloning operates entirely in RAM and can be added or changed at any time without affecting persisted data.
Indexes
Indexes improve read performance (O(1) and O(log n) lookups) at the cost of maintenance on every insert, update, and delete. Index configuration exists only in RAM and can be added or removed at any time.
On small datasets or write-heavy, read-light workloads the maintenance overhead may outweigh the lookup benefit — prefer full scans until profiling shows otherwise. The identity index in particular is disabled by default for this reason; enable it when by-id lookups are on the hot path.
Cache maintenance
Call PurgeCache periodically in long-running instances to reclaim memory from accumulated stale and deleted records. Use ResolveStatistics with Stats.StaleCount | Stats.DeletedCount to drive this — for example, purge when the stale + deleted count exceeds a chosen percentage of the fresh count.
Partitioning across datasets
For workloads where cache-side lock contention becomes the bottleneck, consider partitioning your data into multiple datasets keyed on a domain attribute and opening them as separate MemDb<T> instances in the same process. Each instance has its own lock, its own indexes, and its own persister, so contention scales horizontally.