Overview
Temporal versioning is an optional engine-level feature that replaces destructive overwrites with append-only version chains. When enabled, each write to a key creates a new version instead of replacing the existing value. Every version stores the value, a monotonic timestamp, the writer ID, and the operation type. The result is a complete, queryable history of every value a key has ever held, within a configurable retention window.
The primary use cases are production debugging (what did the cache serve during an incident?), compliance auditing (prove data accuracy at a specific timestamp), and operational forensics (detect and trace cache poisoning). Standard GET operations are unaffected — temporal versioning adds zero overhead to the hot read path.
Enable temporal versioning when you need historical cache state for debugging, compliance, or forensic analysis. For workloads that do not require version history, leave it disabled to avoid the storage overhead of maintaining version chains.
Version Storage
Each key maintains an append-only log of versions, ordered newest to oldest. Every write appends a new entry to the head of the log. Previous values are never overwritten or mutated.
Version Entry Structure
Each version is a fixed-size metadata struct pointing to the value payload:
The version chain is a singly-linked list. New versions are prepended at the head via atomic CAS, identical to the MVCC write path. The chain is ordered by timestamp, newest first, which enables efficient binary search for time-travel queries.
Once written, a version entry is immutable. It can only be removed by the garbage collector when it exceeds the retention policy. This property is critical for audit compliance: the version chain is a tamper-evident log of cache state.
Commands
Temporal versioning introduces four new commands. All existing commands (GET, SET, DEL, etc.) work identically — no client code changes required.
| Command | Complexity | Description |
|---|---|---|
GET key AT ts |
O(log n) | Binary search on version chain. Returns the version with the largest timestamp ≤ ts. |
HISTORY key [LIMIT n] |
O(n) | Linear scan of version chain head to tail (or up to LIMIT). Returns full version metadata. |
DIFF key t1 t2 |
O(log n + k) | Binary search to t1, then linear scan to t2. k = number of versions in the range. |
VERSIONS key |
O(1) | Maintained as a counter on the key metadata. No chain traversal. |
Retention Configuration
Retention is configurable globally and per key prefix. The garbage collector respects these policies and automatically removes expired versions. All configuration is runtime-modifiable — no restart required.
| Parameter | Default | Description |
|---|---|---|
temporal.enabled |
false | Enable or disable temporal versioning. When disabled, writes overwrite in place (standard behavior). |
temporal.retention.default |
24h | Default retention period for version chains. Versions older than this are eligible for GC. |
temporal.retention.prefix:* |
— | Per-prefix retention override. Keys matching the prefix use this retention instead of the default. |
temporal.gc_interval_ms |
1000 | How often the background GC thread scans version chains for expired entries, in milliseconds. |
Setting temporal.enabled false at runtime triggers a GC pass that collapses all version chains to single (current) versions. This is non-blocking but may take several GC cycles to complete. During the transition, time-travel queries return results from the remaining chain until it is fully collapsed.
Storage Tiering
Temporal versioning composes with hybrid tiering to manage version storage across memory tiers. The tiering engine automatically promotes and demotes versions based on age and access patterns.
| Storage Tier | Version Age | Access Latency | Use Case |
|---|---|---|---|
| RAM | Current + recent | ~0.5µs | Active debugging, real-time queries |
| NVMe | Hours to days | ~10µs | Post-mortem debugging, 7-day lookback |
| Cold Archive (S3/GCS) | Days to years | ~100ms | Compliance retention, FINRA/SOC 2 audits |
Demotion is automatic: when a version ages past the RAM retention threshold, it is written to NVMe. When it ages past the NVMe threshold, it is archived to cold storage. Promotion happens on access: a GET key AT timestamp query that hits an NVMe-resident version promotes it to RAM for the duration of the query.
Memory Overhead
Each version adds 32 bytes of metadata overhead. The value payload is stored separately and sized by the application. The table below summarizes metadata-only overhead at common scales.
| Keys | Versions / Key / Day | Retention | Metadata Overhead | With 1KB Avg Value |
|---|---|---|---|---|
| 1M | 24 | 24h | 768 MB | 24.8 GB |
| 10M | 24 | 24h | 7.68 GB | 248 GB |
| 10M | 24 | 7d | 53.8 GB | 1.7 TB (NVMe tiered) |
| 1M | 100 | 24h | 3.2 GB | 103 GB |
For capacity planning, compute: keys × versions_per_day × retention_days × (32 + avg_value_size). Use hybrid tiering to keep only recent versions in RAM. At 10M keys with 24h retention, the 7.68 GB metadata overhead is the cost of full version history. With NVMe tiering for the value payloads, only the 7.68 GB metadata needs to reside in RAM.
Performance Impact
Temporal versioning is designed to have zero impact on the standard read path and minimal impact on writes.
| Operation | Without Temporal | With Temporal |
|---|---|---|
| GET (current value) | ~0.0015ms | ~0.0015ms (unchanged) |
| GET key AT timestamp | — | ~0.5µs (binary search) |
| SET (write) | ~0.013ms | ~0.014ms (+0.001ms) |
| HISTORY key (10 versions) | — | ~1µs |
| DIFF key t1 t2 (5 versions) | — | ~0.8µs |
| VERSIONS key | — | ~0.05µs (counter read) |
The standard GET is completely unchanged because it reads only the head of the version chain (the current value), which is always in RAM. Write latency increases by approximately 0.001ms for version allocation and atomic CAS prepend. Time-travel queries (GET AT) perform a binary search on the in-memory portion of the version chain, completing in approximately 0.5 microseconds for chains of up to ~1000 versions.
Garbage Collection
Temporal versioning uses a background garbage collector to reclaim expired versions. The GC is non-blocking and runs on a dedicated thread.
GC Behavior
- Scan interval: Every
temporal.gc_interval_msmilliseconds, the GC thread scans a batch of keys with version chains. - Retention check: For each key, the GC computes the applicable retention period (prefix-specific or default). Versions older than the retention period are marked for removal.
- Tiering check: Before removal, versions within the retention window but beyond the RAM age threshold are demoted to NVMe or cold archive.
- Reclamation: Expired versions are unlinked from the chain and their memory is returned to the allocator. This operation is non-blocking — readers traversing the chain see a consistent snapshot via epoch protection.
Storage grows proportionally to write_rate × retention_period. A key written 1000 times/second with 90-day retention will accumulate approximately 7.78 billion versions (249 GB metadata alone). For high-write keys with long retention requirements, consider using prefix-specific retention to limit version accumulation, or use sampling (retain every Nth version) for operational data while keeping full history only for audit-critical prefixes.
Limitations
Temporal versioning adds storage overhead that must be planned for. It is not a universal optimization.
- Storage growth: Version storage grows with
write_rate × retention_period × (32 + avg_value_size). High-write keys with long retention need explicit capacity planning. - RAM for metadata: Version metadata (32 bytes/version) resides in RAM even when values are tiered to NVMe. At scale, this metadata can be significant.
- GC latency for long chains: Keys with millions of retained versions have longer GC scan times per key. The GC is non-blocking, but scanning a 1M-version chain takes measurably longer than a 100-version chain.
- Cold archive latency: Queries against cold-archived versions incur object-store latency (~100ms). For real-time debugging, ensure sufficient RAM or NVMe retention to cover the relevant time window.
- Not a database: Temporal versioning provides version history for cached values. It is not a replacement for database transaction logs, WAL, or event sourcing. Use it for cache-layer debugging and compliance, not as a primary audit system.
Start with temporal versioning enabled on audit-critical key prefixes only (temporal.retention.prefix:audit 90d). Expand to operational prefixes (user:*, pricing:*) with shorter retention (24h–7d) as you validate storage growth. Use VERSIONS key and the temporal.total_versions metric to monitor chain lengths.