advanced 60 min

Caching & resource management

The cache layer that keeps hot nodes in memory, and the memory / disk / throughput characteristics of a production node.

Prerequisites

Complete these before starting this module:

What you'll learn

  • Explain the multi-tier caching and the cache-hit path.
  • Reason about memory, disk and write-throughput budgets.
  • Tune cache size and age for different deployments.
Complete this module by self-assessment and a quiz. Jump to assessment

Introduction

≈60 min · Advanced · builds on Storage backends & database operations

Disk is slow, and a busy node can't afford to hit it for every lookup, so it caches aggressively. In this module you'll learn the multi-tier caching that keeps hot nodes in memory, how the TaggedCache decides what to keep and evict, and how to reason about a node's memory, disk and throughput budgets. It's the difference between a node that keeps up and one that falls behind.

The Performance Problem

In brief: hitting disk for every node lookup would be far too slow, so hot nodes stay in memory.

Scenario: Synchronizing from the Network

A new node joins XRPL and must catch up to current ledger. This requires:

  1. Fetching missing ledgers (blocks of transactions)
  2. For each ledger, fetching all state nodes
  3. Verifying each node's hash
  4. Storing nodes to disk

Naive Approach (No Cache):

for (each node in ledger) {
    backend.fetch(nodeHash);  // Database query - 10ms
}

For 10,000 nodes in ledger:
  10,000 × 10ms = 100 seconds per ledger

For 100,000 ledgers to catch up:
  100 seconds × 100,000 = 1,157 days

Clearly infeasible.

With Caching (90% Hit Rate):

Still slow, but realistic with parallel processing.

The difference between possible and impossible is caching.

Key idea. A cache hit serves a node straight from memory with no disk read. Cache hit-rate is one of the biggest levers on a node's real-world performance.

TaggedCache Architecture

In brief: the cache that holds nodes by hash and sweeps them out by age.

The NodeStore's primary cache is the TaggedCache:

Purpose:

class TaggedCache {
    // Keep frequently accessed NodeObjects in memory
    // Minimize expensive database queries
    // Provide thread-safe concurrent access
};

Structure:

Cache Tiers:

NodeStore implements a two-tier caching strategy:

Two cache tiers: L1 is the in-memory TaggedCache (1 to 10 microseconds, 100 MB to 5 GB configurable); on a miss the request falls to the L2 backend database (1 to 10 milliseconds, unlimited size) and finally to disk

Fetch Algorithm:

Cache Insertion Strategy

When Objects Enter Cache:

Three cache insertion strategies: on database fetch (always cache the result, an object on success or a dummy on failure), on storage (cache immediately, fresh nodes get reaccessed), and predictive loading (prefetch likely-needed siblings while traversing)

Dummy Objects:

Special marker objects prevent wasted lookups:

Benefit of Dummies:

Prevents thundering herd of repeated failed lookups.

Cache Eviction

In brief: how the cache decides what to drop when memory is tight.

Cache capacity is limited. When full, old objects must be evicted.

Eviction Triggers:

LRU (Least Recently Used) Eviction:

Age-Based Eviction:

Configuration Parameters:

// From xrpld.cfg
[node_db]
cache_size = 256        // MB
cache_age = 60          // seconds

Impact of Configuration:

Small cache:
  cache_size = 32 MB
  Hit rate: ~60% (more evictions)
  Disk queries: 40% of lookups (slower)

Large cache:
  cache_size = 1024 MB
  Hit rate: ~95% (fewer evictions)
  Disk queries: 5% of lookups (faster)
  Memory usage: Higher

Operators choose based on available RAM and performance needs

Cache Performance Metrics

NodeStore tracks cache effectiveness:

Example Metrics:

Synchronization Optimization

During network synchronization, special techniques optimize caching:

Prefetching

Batch Loading

// Fetch multiple nodes in single operation
std::vector<uint256> hashes = {hash1, hash2, hash3, ...};
auto results = backend->fetchBatch(hashes);

// Reduces backend overhead
// Populates cache efficiently
// Parallelizes I/O operations

Deferred Reads

// During synchronization, identify missing nodes
std::vector<uint256> missing = getMissingNodes(shamap);

// Request from peers asynchronously
// When they arrive, cache them
// Continue traversal without blocking on network

// This allows pipelining: request more while processing previous results

Working Set Management

Different phases of operation have different access patterns:

During Normal Operation (Steady State)

Access pattern: Recent ledgers frequently accessed
  Root hash: checked at consensus
  Recent state nodes: queried for transactions
  Old historical data: rarely accessed

Cache configuration:
  Keep recent ledgers fully cached
  Let old ledgers evict to make room
  Result: Excellent hit rate for current operations

During Synchronization

Access pattern: Missing nodes from network
  Need to verify hash chain from root to leaf
  Often fetching siblings (related nodes)
  May access same node multiple times

Cache strategy:
  Smaller cache acceptable (still beneficial)
  Prefetch siblings when fetching parent
  Use dummy markers to avoid retry storms
  Result: Synchronization completes in hours vs days

After Sync Completion

Access pattern: Back to steady-state recent ledger access

Cache characteristics:
  Most-accessed nodes pinned in cache
  Hit rate quickly reaches 90%+
  Warm cache from prior work

Thread Safety

In brief: how the cache stays correct while many threads read and write it.

Cache must be safe for concurrent access:

Concurrency Properties:

Multiple readers:
  Many threads can fetch simultaneously
  No contention for cache hits
  Scaling: hundreds of concurrent fetches possible

Insert/evict operations:
  Exclusive lock for modification
  Short-lived (just map operations)
  Background eviction: doesn't block fetches

Advanced Caching: Full Below Optimization

During synchronization, special tracking prevents redundant work:

The Problem:

The redundant-check problem: syncing walks the tree comparing hashes, and once a subtree is complete it would still be re-checked when new nodes arrive; the fullBelow marker records proven-complete subtrees so those walks are skipped

The Solution: Full Below Generation Counter

Benefit:

When synchronizing:
  Fetch subtree, verify all descendants present
  Mark as "full below" with generation ID
  Later sync process checks generation
  If matches current: skip this subtree (known complete)
  If differs: need to re-verify (new sync started)

Result: Avoids re-traversing known-complete subtrees
Significant speedup in incremental sync scenarios

Resource Management and Performance Characteristics


Introduction

Understanding SHAMap and NodeStore theoretically is one thing. Operating them in production is another.

This final chapter covers:

  • Real-world resource requirements
  • Performance measurement and optimization
  • Bottleneck identification
  • Tuning for different deployment scenarios

Lookup Complexity Analysis

SHAMap Lookup

Batch Fetch

N objects requested:

Naive (sequential):
  N × database_latency = N × 10ms
  Example: 100 objects = 1000ms

Batched:
  single_batch_latency + deserialize
  Example: 100 objects = 10ms + 5ms = 15ms
  Speedup: 66x

Write Throughput

Ledger Close Cycle

Anatomy of a 3.5 to 5 second ledger close: about two seconds receiving and executing transactions, one second of consensus, and half a second to one second writing to the NodeStore.

Node Object Volume

Write Latency

Write latency on a log scale: a cache update costs 1 to 10 microseconds, encoding about 100 microseconds, an SSD write 100 to 1000 microseconds, and batch accumulation 10 to 100 milliseconds.

Read Performance Characteristics

Cache Hit Scenario

Hit rate: 95% (well-tuned system)

1000 object requests:
  950 cache hits × 5 microseconds = 4.75 milliseconds
  50 cache misses × 10 milliseconds = 500 milliseconds
  Total: 504.75 milliseconds = 0.5 seconds

Average per request: 0.5 milliseconds

Cache Miss Scenario

Hit rate: 60% (poorly tuned system)

1000 object requests:
  600 cache hits × 5 microseconds = 3 milliseconds
  400 cache misses × 10 milliseconds = 4000 milliseconds = 4 seconds
  Total: 4.003 seconds

Average per request: 4 milliseconds

10x slower due to cache misses!

Memory Requirements

NodeStore Memory

SHAMap Memory

In-memory tree of current + recent ledgers:

Active ledger: ~10-50MB
  Depends on account count and modification volume

Recent immutable ledgers (kept for quick access):
  2-3 most recent: ~30-150MB

Total SHAMap: 50-200MB typical

Plus cached nodes (shared with NodeStore cache):
  Counted above in NodeStore memory

Total Memory Budget

Disk Space Requirements

Database Growth

With Rotation

Retention policy: Keep last 100,000 ledgers

Ledger creation rate: 1 ledger per ~3 seconds
100,000 ledgers = ~8 days of history

Database size:
  100,000 × 0.2MB = 20GB (stable)
  With overhead: 30-50GB typical

Bounded growth enables indefinite operation

Actual Sizes on Mainnet

Small validator (RocksDB, compressed):
  Database: 30-50GB
  With binaries/logs: 60GB total

Archive node (full history):
  Database: 500GB-1TB
  With redundancy: 1.5TB total

Growth per day (with rotation):
  ~500MB-1GB per day
  (old data deleted as new data added)

File Descriptor Usage

File Descriptor Requirements

Performance Tuning

Identifying Bottlenecks

Monitor these metrics:

Tuning Parameters

Scenario 1: High-Traffic Validator

Problem: Write latency too high (ledgers close slowly)

Solution:
  - Increase cache_size to 1GB+
  - Switch to NuDB backend (higher throughput)
  - Increase async_threads to 8-16
  - Ensure SSD (not HDD)
  - Increase batch_write_size

Result: Write throughput 50K+ objects/sec

Scenario 2: Memory-Constrained

Problem: Only 512MB RAM available

Solution:
  - Set cache_size = 64MB (small)
  - Still runs, but slower
  - Increase cache_age for working set
  - Monitor hit rate (may drop to 80%)

Result: Functional but slower sync and queries

Scenario 3: Archive Node

Problem: Need complete history, very large disk

Solution:
  - No rotation (online_delete disabled)
  - RocksDB with compression
  - Smaller cache_size (less frequently accessed)
  - Parallel database with rotated copy

Result: Full history, terabyte+ database

Performance Characteristics Summary

Lookup Performance:

Single object lookup:
  Cache hit:    1-10 microseconds
  Cache miss:   1-10 milliseconds
  95% hit rate: ~0.5 milliseconds average

Batch operation (100 objects):
  Sequential:   1000 milliseconds
  Batched:      10 milliseconds
  Speedup:      100x

Write Performance:

Per ledger:
  1000 objects per ledger
  Per-object: 0.1-1 millisecond
  Batch overhead: 10-100 milliseconds
  Total per ledger: 100-1100 milliseconds

Throughput:
  4 ledgers/second × 1000 objects/ledger = 4000 obj/sec
  Well within RocksDB/NuDB capacity

Memory Usage:

Minimum:  200-300MB
Typical:  500-700MB
Large:    2-4GB
Depends on cache_size configuration

Disk Space:

With rotation:  30-50GB (8-10 days history)
Unbounded:      ~1TB per year (without rotation)
Growth rate:    ~500MB-1GB per day

Scalability Limits:

Monitoring in Production

Key Metrics to Track

Alerting Thresholds

Warning:
  - Hit rate < 80%
  - Write latency > 200ms
  - Queue depth > 5000

Critical:
  - Hit rate < 60%
  - Write latency > 500ms
  - Ledger close > 10 seconds
  - Disk space < 10GB free

Summary

This module explained how a node stays fast under load. A multi-tier cache keeps hot nodes in memory so the node rarely touches disk; the TaggedCache holds them by hash and sweeps them out by age. You learned to reason about a node's memory, disk, and throughput budgets, and saw why cache hit-rate is one of the biggest levers on real-world performance.

To remember:

  • TaggedCache: an in-memory, hash-keyed cache with age-based sweeping
  • Cache hit = served from RAM; miss = NodeStore (disk) fetch, then insert
  • get_counts (admin) shows cache sizes and hit rates: measure before you tune
  • Tune size and age per deployment; a validator and a full-history node want different budgets
  • The hot working set is the recent ledgers' nodes; sync workloads shift it
  • fullBelow tracking marks complete subtrees during sync so they are not re-walked
  • Cache hit-rate is one of the biggest real-world performance levers
  • Watch out: a bigger cache is not free (RAM pressure, sweep cost); measure with get_counts before and after

Next up. Storage is fast enough; time to spend that speed. Next phase of the machine: the transactor architecture, the framework every transaction type plugs into.

Assignments

0 of 2 complete

Unlocks

Finishing this module opens up:

XRPL Academy © 2026