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.

When to Enable

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:

Rust struct TemporalVersion { value: Bytes, // the cached value (heap-allocated) timestamp: u64, // monotonic nanosecond timestamp writer_id: u32, // identifies the writing server/service operation_type: u8, // SET=1, DEL=2, CDC=3, HEAL=4 next: *mut TemporalVersion, // pointer to previous version } // Metadata overhead per version: 32 bytes // timestamp (8) + writer_id (4) + op_type (1) + padding (3) + next ptr (8) + len (8) // Value payload is a separate heap allocation, not counted in version overhead

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.

Append-Only Guarantee

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.

Commands # Time-travel query: return the value at a specific timestamp GET key AT timestamp # Returns: the value that was active at the given timestamp # Timestamp format: ISO 8601 (2026-03-27T03:00:00) or Unix epoch nanoseconds # Version history: return the timeline of all retained versions HISTORY key [LIMIT n] # Returns: array of {value, timestamp, writer_id, operation_type} # Ordered newest-first. LIMIT restricts the number of entries returned. # Diff between two time points DIFF key t1 t2 # Returns: list of versions that changed between t1 and t2 # Includes the value, timestamp, and writer for each change # Version count: how many versions are retained for a key VERSIONS key # Returns: integer count of retained versions
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.

Config Commands # Enable temporal versioning (default: false) CONFIG SET temporal.enabled true # Default retention period (default: 24h) CONFIG SET temporal.retention.default 24h # Per-prefix retention overrides CONFIG SET temporal.retention.prefix:audit 90d CONFIG SET temporal.retention.prefix:session 1h CONFIG SET temporal.retention.prefix:txn 2555d # GC scan interval in milliseconds (default: 1000) CONFIG SET temporal.gc_interval_ms 1000
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.
Disabling Temporal Versioning

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
Capacity Planning

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

  1. Scan interval: Every temporal.gc_interval_ms milliseconds, the GC thread scans a batch of keys with version chains.
  2. 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.
  3. Tiering check: Before removal, versions within the retention window but beyond the RAM age threshold are demoted to NVMe or cold archive.
  4. 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.
High-Write Keys with Long Retention

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.
Recommendation

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.