Performance and memory tuning
Grandine is designed to run well with its defaults. Most operators never need this page. Read it if memory use is higher than you expect, if you run on small hardware, or if the network stops finalizing.
How Grandine uses memory
Grandine keeps fork choice in memory. Everything after the last finalized checkpoint (unfinalized blocks and the beacon states after them) lives in RAM, and finalized data goes to the database on disk. In a healthy network finality is about two epochs behind the head, so the in-memory part is small and stable.
The main consumers of memory are:
- Unfinalized states. Grandine keeps the post-state of recent unfinalized blocks in memory, so
it can process new blocks and attestations without rebuilding states. States more than
--unfinalized-states-in-memoryslots behind the head are unloaded. States at epoch boundaries are written to the database first, so they can be loaded back if needed. - The state cache. States computed for other reasons (for example the state at a given slot for
the API or for attestation validation) are cached. States older than
--max-epochs-to-retain-states-in-cacheepochs behind the head, or older than the finalized slot, are pruned once per epoch. - Checkpoint states for unfinalized checkpoints, dropped as finality advances.
- Operation pools (attestations, sync committee messages and others), the pubkey cache, and network buffers.
Beacon states share most of their data with each other through persistent data structures, so one more state in memory costs much less than a full copy.
Grandine needs about 5 GB of memory on a healthy Mainnet. Actual use depends on the network, the number of validators, the options and the version, so measure it on your own node.
Measuring memory correctly
Tools like top and ps show the resident set size (RSS), which includes memory the allocator
holds but the application is not using, and pages the kernel has not reclaimed yet. On a machine
with plenty of free RAM, RSS overstates what Grandine needs.
To see how much memory Grandine really needs, put the system under memory pressure so the kernel
reclaims what it can. The stress-ng tool can do that
by allocating 90% of the available memory:
stress-ng --vm-bytes $(awk '/MemAvailable/{printf "%d\n", $2 * 0.9;}' < /proc/meminfo)k --vm-keep -m 1
Watch Grandine's RSS while it runs, then stop it with Ctrl+C.
Warning: Do this on a test machine, or at a time when a short slowdown is acceptable. Memory pressure can make every process on the host slow, including the execution client, and the kernel's out-of-memory killer can end processes if you overshoot.
Over time, the metrics give a better picture:
GRANDINE_USED_MEMORY: the process's memory as reported by the OS.JEMALLOC_BYTES_ALLOCATED: bytes the application has allocated and not freed.JEMALLOC_BYTES_RESIDENTandJEMALLOC_BYTES_RETAINED: memory the allocator holds from the OS.
If JEMALLOC_BYTES_ALLOCATED is stable while RSS is higher, the difference is allocator overhead
and memory not yet returned to the OS, not a leak. The Overview Grafana dashboard plots these.
The allocator
Grandine uses jemalloc (through tikv-jemallocator) as its global
allocator on every platform except Windows with the MSVC toolchain, which uses the system
allocator. jemalloc reduces fragmentation in a long-running, multi-threaded process like a beacon
node.
Grandine builds jemalloc with prefixed symbols, so it reads tuning options from the
_RJEM_MALLOC_CONF environment variable, not MALLOC_CONF. The defaults suit most operators.
Memory options
--unfinalized-states-in-memory
How many slots of unfinalized states to keep in memory behind the head. Default: 128 (four epochs
on mainnet). The minimum is two epochs, 64 slots on mainnet; lower values are refused with
--unfinalized-states-in-memory must be at least 64.
- Lower it toward
64to save memory, at the cost of more state loading and recomputation when older states are needed, for example during reorgs or for late attestations. - Raise it if you have spare RAM and see recomputation slowing the node down.
grandine --network mainnet --unfinalized-states-in-memory 64 ...
--max-epochs-to-retain-states-in-cache
How many epochs behind the head the state cache keeps states. Default: 8. A lower value saves
memory when the cache holds many states, for example on a node that serves many API requests for
recent states. The cache never keeps states older than the finalized slot, whatever this value is.
--state-cache-lock-timeout
How long, in milliseconds, a task waits for the lock on a state cache entry before it gives up and
computes the state itself. Default: 1500. You should not need to change it.
--max-empty-slots
How many empty slots Grandine is willing to process on top of a state. Default: 32. If the chain
has a longer run of slots without blocks, for example after a network outage, building a state
fails with too many empty slots after state, and validators stop performing duties on a head that
far behind. Raise it only in such an exceptional situation.
--database-size
The maximum size of the beacon database. Default: 1 TiB. This is an upper limit for the memory
mapping, not space that is allocated in advance. If the database reaches the limit, Grandine
restarts its runtime with a doubled limit and logs increasing environment map size limits.
See Storage and database.
CPU and thread pools
Grandine parallelizes most work. You do not configure thread counts; Grandine sizes its pools from
the number of CPUs it sees (nproc):
- Fork choice thread pool. One worker per CPU runs fork choice tasks: block processing, attestation and aggregate validation, blob and data column sidecar handling, and state preprocessing. Tasks are prioritized: blocks, blob sidecars and checkpoint states first, then data column sidecars, then attestations, aggregates and background persistence.
- Attestation verification verifies signatures in batches, which is much cheaper than one at a time.
- Dedicated executors run CPU-heavy jobs away from the async runtime that handles networking
and APIs:
de-normal: one thread per CPU, normal priority, for block production and similar work.de-low: a quarter of the CPUs (at least one), at the lowest scheduling priority (nice 19), for background work.de-recover(a quarter of the CPUs) andde-reconstruct(one thread), both at nice 19, for data column recovery and reconstruction.
- Rayon for data-parallel loops.
Thread names appear in top -H and in panics, which helps when you investigate CPU use. The
dedicated executor task counts and timings are on the Overview dashboard.
To limit how many CPUs Grandine uses, restrict the process instead of looking for a flag, for
example with taskset, the systemd CPUAffinity= setting, or Docker's --cpuset-cpus. Pools are
sized from the CPUs available to the process. A cgroup CPU quota, such as Docker's --cpus or the
systemd CPUQuota= setting, also counts: the pools use the quota rounded up to whole CPUs, or the
affinity count if that is lower.
Other options that affect CPU use:
--subscribe-all-subnetsand--supernodemake the node process far more gossip. Use them only when you need them. See Networking and peers and PeerDAS and blob data.--use-validator-key-cachespeeds up startup with many local keystores. Keystore decryption is deliberately slow; the cache stores the keys in a single encrypted file in the validator directory, which is faster to load. See Managing validator keys.
Disk
Grandine writes finalized data in batches and reads states from disk when it needs them. A fast
SSD (NVMe preferred) matters more than raw capacity. Archive nodes (--archive-storage) and
back-sync write much more. See System requirements and
Storage and database.
Long periods of non-finality
When the network stops finalizing, nothing becomes finalized, so the unfinalized part of fork choice keeps growing. With an in-memory fork choice, that shows up as rising memory use, and more state writes as Grandine persists the epoch boundary states it unloads.
If this happens:
-
Stay on the latest release. Client teams often ship fixes during incidents. Watch the Grandine Discord and the release page. See Updating Grandine.
-
Watch memory and disk. Track
GRANDINE_USED_MEMORY,SYSTEM_USED_MEMORYand free disk space, and alert before they run out. -
Lower the in-memory limits if memory gets tight:
grandine --network mainnet \ --unfinalized-states-in-memory 64 \ --max-epochs-to-retain-states-in-cache 2 \ ...Lower values trade memory for CPU and disk reads, as states are reloaded or recomputed more often.
-
Add swap or RAM as a last resort, so the out-of-memory killer does not stop the node.
-
Do not resync from a checkpoint during non-finality unless you have to. A checkpoint sync starts from the last finalized state, which may be far behind, and the node then has to process every unfinalized block again.
-
Keep validators running. Validators that keep attesting help the network finalize again, and inactivity penalties grow while it does not.
Quick checklist
- Memory looks high: measure it under memory pressure (above) before you change anything.
- Small machine:
--unfinalized-states-in-memory 64and--max-epochs-to-retain-states-in-cache 4. - CPU spikes: enable the
WarnOnFailedServiceHealthChecksandWarnOnStateCacheSlotProcessingruntime features to find the cause, and include the logs in a report. - Slow startup with many keys:
--use-validator-key-cache.
Command line options
See Command line reference for the full list.
--unfinalized-states-in-memory: default128. Slots of unfinalized states kept in memory behind the head. Minimum: two epochs.--max-epochs-to-retain-states-in-cache: default8. Epochs behind the head that the state cache keeps states for.--state-cache-lock-timeout: default1500. State cache lock timeout in milliseconds.--max-empty-slots: default32. Maximum number of empty slots processed on top of a state.--database-size: default1 TiB. Maximum size of the beacon database.--use-validator-key-cache: default disabled. Cache validator keys in an encrypted file for faster startup.--in-memory: default disabled. Keep all data in memory and nothing on disk. For testing only.