Storage & sealing
Nuthatch stores data in two tiers, split at the finality boundary - and glues them behind one SQL surface.
Hot: the redb tip store
Recent, still-reorgable blocks live in an embedded redb key-value store - a single table keyed by
(block_number, log_index), each row tagged with the event table it belongs to, so a reorg rollback is
a cheap range-delete above the fork block. This is the mutable tier: it’s the only place a reorg ever
lands. Entity point-reads (/entity/{id}) hit it directly.
Cold: content-addressed Parquet
Once a block range passes finality (a conservative depth), its rows are sealed to an immutable,
content-addressed (sha256) zstd Parquet segment under segments/, catalogued in
manifest.json with block bounds and row count. Segments sealed before 3.7.0 are Snappy; a nest
holding both serves across the seam. A monotonic sealed_through watermark advances so each
range seals exactly once, and the sealed rows are then pruned from the hot store. A table with fewer
than 1,000 final rows (or 16 MiB) at a cut is sealed provisionally and folded into its next cut, so a
quiet nest keeps its tip in redb a while before its first segment appears.
Because a segment’s identity is a hash over its bytes, the same range always produces the same segment - on any machine, on any run. That’s what makes segments reusable across nest versions (see Upgrading a nest) and shareable as a verifiable cache.
Sealed is immutable, forever. Segments are sealed strictly past finality, so the columnar layer is append-only. If a change would require mutating a sealed segment, the design is wrong - go back. A reorg can never reach a sealed segment, by construction.
The union: one engine over hot ∪ cold
Analytical SQL (/sql, nuthatch sql) runs on Burrmill,
an embedded query engine on DataFusion. Each session registers the sealed segments and the hot tip’s
rows as one table per event, so a query spans all of history seamlessly, hot and cold. Only the
ingestion thread writes; queries only read.
Burrmill has been the only engine since 4.1.0. Releases up to 4.0.2 used DuckDB, which is no longer in the binary; the storage tiers did not change, only the engine reading them. Because sealed segments are plain Parquet, external readers such as DuckDB or Trino can read them directly. See Replacing DuckDB, after all.
A point-read for a pruned id transparently falls back to the cold path, so /entity/{id} works across
the hot→cold seam without the caller knowing where the row lives.
Big integers
Values wider than 64 bits (a uint256) are stored as an exact decimal string, with a derived
{col}_dec DECIMAL column for numeric use. A value over 38 digits exceeds DECIMAL(38,0), and neither
cast rescues it exactly: HUGEINT is signed 128-bit and overflows at the same order, and DOUBLE
loses precision past about 15 digits. See The SQL surface.
Next
- Reorgs & finality - the boundary this split is built on
- Determinism - why content-addressing matters
- The SQL surface - querying the union
Checked against Nuthatch 5.8.2. Verify a release-specific command with the CLI reference before running it in production.