Repository navigation
Releases: nuthatch-org/nuthatch
Release list
v5.3.0
Nuthatch 5.3.0 proves which blocks a validator proposed (RFC-0063 plan C), whoever received their
fees, so rotki can book a validator's produced blocks and MEV from a nest alone. It also fixes a 5.2.0
defect that stopped rotki reading history from a nest with a start_block. Nothing outside an
[address_history] nest changes, and there is no storage migration.
Proven proposers per validator (Ethereum mainnet)
- The proof is the chain's own. Block N+1's
parent_beacon_block_root(EIP-4788) commits to the
beacon header that produced block N, whoseproposer_indexnames the proposer. Every row of a span
is checked before any is filtered by validator, so a tampered row fails the span instead of hiding a
proposal. Every span hangs from the block the nest's own RPC calls finalized, read by that tag. - Coverage: slot 12,777,208 (block 23,551,570) onward. A range starting earlier is refused with
"proposals are proven from block 23551570". Proofs do not followstart_blockorend_block;
end_blockbounds only what is served. - Where the proofs come from: catalogue files beside the block partitions on the mirror (published
for blocks 23,551,570 to 26,159,999), and for the blocks above them, built here from the RPC and a
beacon node whenbeacon_url(orNUTHATCH_BEACON_URL) names one. Without one, those blocks stay
incomplete. - API:
validators = [...], orPOST /api/watchwith{"validator": N};
account/nuthatchValidatorBlocks&validator=N&startblock&endblock&page&offset, complete or refused,
withsupportedFromandverifiedThrough. Rows carry the block reward (verified from the block's
receipts, orrewardStatus: unavailableand retried, never zero), the relays' MEV, and the builder's
payment from the block's traces. nuthatch partitions --proposers --beacon-url URL --beacon-cache DIRbuilds the catalogue for a
mirror from its block partitions, verifying every header as a nest does.
Fixes
- A range before
start_blocknames where history starts (fixes 5.2.0). Every account action
refused a range starting beforestart_block, including rotki's usualstartblock=0and a request
with no start, as "not indexed at block 0", which rotki could not act on. It now says "history starts
at block N", and rotki asks again from there. - A covered answer above the finalized head is served on one hash check instead of waiting for the
cursor. Measured with the cursor busy backfilling another account: every request timed out at 20 s
before; 0.08 to 0.53 s after. logs/getLogsfetches a page's receipts and timestamps together, 16 at a time, and answers
within the request deadline or refuses.- A request above what is covered is served inside a long backfill. The cursor serves such
requests between its windows, and a window could cover 50,000 blocks: on mainnet, tip requests for
one account while the nest backfilled a busy wallet waited 10.9 to 16.0 s each. Backfill windows now
start at 128 blocks and are paced to a quarter of the request deadline, and the bridge to the
finalized block serves waiting requests between its chunks. The same measurement: 0.05 to 2.6 s,
except a first request right after startup, which can still wait for the first windows. request_deadlinedefaults to 8 s, down from 20 s, and is one ceiling per request from its
arrival: the hash check, the queue, a window the cursor is part way through, a trace and a cover all
count against it. A fully covered range is served if its final read completes within that ceiling;
otherwise the request is refused. A gap the cursor cannot cover in time at its measured pace is
refused at once with how far history is covered. The pace is measured per layer (discovery, block
bodies, produced blocks), the layers' times add up, and owed relay re-asks count as blocks.- Recent timestamp lookups search near the chain head first. rotki's usual timestamp just
behind the tip is bracketed within eight blocks, avoiding a search from genesis that could exhaust
the four-second search budget. The answer still passes the same neighbour and ancestry checks. - An open mirror is read as far as it has published. A
[address_history.mirror]without
to_blockfollows the mirror's newest partition; coverage it cannot verify is taken back first.
Compatibility and upgrade
Replace the binary and restart. A 5.2.0 store is used as it is. Validator watches start empty.
v5.2.0
Nuthatch 5.2.0 makes address history (RFC-0063) meet rotki's sync on Ethereum mainnet: history up to
the chain head on demand, reorgs taken back, and answers that are exact or refused. Nothing outside an
[address_history] nest changes. An address-history store written by 5.1 is moved aside intact and
the history read again (see Compatibility).
Two cold tests of 5.1.1, by testers with no build history, said not ready. Every item below answers one
of their findings. RFC-0063 §12 has the reasoning.
Exact or refused
- An explicit
endblockis answered whole or refused. 5.1.1 shortened anendblockabove its
head to the head and answeredstatus 1, so a client recorded history it never received: 17 of
Etherscan's 65 rows came back as complete. Now the answer is the whole range or
NUTHATCH_INCOMPLETE. Withoutendblockthe answer runs to the end of the covered run, and a page
past the first needs anendblock. getblocknobytimewithout partitions. The 5.1.1 notes said timestamp to block was answered "from
the RPC or from headers-only partitions". It was not: 5.1.1 answered from verified partitions only,
and every nest-only rotki sync without them failed. Now a timestamp the partitions do not bracket is
searched on the nest's own RPC, and the answer is proven against its neighbour block by parent
hash; above the finalized head the answer must also link, parent hash by parent hash, to the
chain head the search began from. A timestamp newer than the chain head is refused, and so is any lookup on a chain whose blocks
can share a second (Arbitrum, BNB Chain).
The tip on demand
rotki ends a sync 15 seconds behind now. The cursor still backfills to the finalized head every poll;
a request above what is covered, and at or below the chain head, is now handed to the cursor, which
covers that address up to the block asked for and answers within request_deadline (new in
[address_history], default 20 s). The cursor stays the only writer: requests wait on a bounded queue
and are served between its windows, so none waits behind another address's backfill. A request past
the chain head is refused. A read reaching above the finalized head also goes through the cursor,
which checks for a reorg first. txhash lookups are traced by the cursor too, up to the chain head,
and refused if the trace names a block the main endpoint does not.
Reorgs
Anything read above the finalized head is read against its window's last block hash, checked before
and after the fetch, together with the newest kept hash under it, and kept with the record. Each poll
and each request above the finalized head compares the newest kept hash with the RPC's, and on a
mismatch takes back everything above the fork: rows, coverage, cached transactions, timestamps,
traces and relay answers. A fetch that began before a reorg keeps nothing above the finalized head,
and a transaction re-included in another block is read again. A fetch is checked against the newest
kept hash under it, so a window read on a fork cannot hide a stale block beneath its own matching
anchor; every block a trace names must be the main endpoint's, finalized or not; and the trace
endpoint must report the main endpoint's hash at each window's last block, so a trace source on
another fork or behind cannot answer an empty range the main chain does not have. Each check is made
twice, so a node that changes fork between comparisons cannot pass. A fetch that fails a check keeps
nothing. A head that fell under a kept block counts as a reorg. Kept hashes survive a restart, and a reorg the chain finalized
while the nest was down is still taken back. Only a block under the
finalized head the store committed is a conflict, which stops the cursor with the reason on /ready.
Finalized history is checked too. The store keeps the hashes of the last 32 finalized heads it
committed, and every poll, the first after a restart included, compares the newest and one older one
in turn with the RPC. A mismatch, which is a reorg under finality or an RPC that came back on another
chain under the same chain id, stops the cursor with the reason on /ready, and every answer is
refused, an answer already being prepared included. The conflict is kept in the store, so a restart
does not clear it; removing the address-history store does. Every fetch is also held against the
newest finalized hash, so a fork switch after a request's own check cannot join two forks. Each
check asks every endpoint in the pool, and the finalized head advances only where they all agree. A new finalized head's
hash is read before and after the checks and committed with the head in one transaction.
Two property tests over random reorg depths gate it, one through the cursor's own window against a
node whose traces follow the chain, with reorgs that cross finality.
rotki's ordinary Etherscan sync can import unfinalized history and does not automatically reconcile
already imported history after a reorg. The nest integration retains this shared limitation.
Produced blocks and MEV
- Payout contracts. Builders that pay the proposer through a payout contract pay as an internal
transfer inside the block's last transaction. Those are now found: the cold test's 16 such payments
over 265,000 blocks had come back as no MEV. unrecorded, notnone. The rows come from the chain; MEV is a label:relay,conflict, or
unrecorded(no delivery at the queried relays when last asked). A relay saves its delivery record
after the block is published and can fail to, sounrecordedblocks and builder payments without a
row are asked again daily for 30 days, and a delivery found then upgrades or inserts the row. The
schedule commits with the coverage, so a crash cannot drop it.- Finalized only. Produced blocks are served through the finalized head; a request past it is
refused with the block it is served through. - First in each pass. Produced blocks and the re-asks are covered before the backfill, so a long
backlog cannot hold them back. - Every leg. A builder payment split across several transactions reports every hash in
paymentTxand their sum inpaymentValue.
Smaller fixes
logs/getLogshonourspageandoffset, starts with 16-block spans that grow only while they
come back sparse, stops once it holds the page, and narrows any span whose answer passes 4 MiB, so
a dense range no longer holds every log in memory; two run at a time. A range that needs more than 64 calls answersNUTHATCH_INCOMPLETE, as
does a failed range, rather than an error.- The block-body scan keeps each block's miner, hash, timestamp and withdrawals for the newest
100,000 blocks, so an address watched later reads those rather than the chain. /readyreportsstalled: truewhile the last poll failed;rpc_requests_totalis current; a
refusal always names its reason;addresses = []is valid; a bind other than loopback warns; and
unwatching one address no longer throws away another's window in flight./readyanswers 503 until the first finalized head is read, still withchain_id.- A span retried after a timeout or a failure logs at warn, and a failed reconcile counts on
poll_errors_total: a nest whose RPC was failing went sixteen minutes without a log line. - The address-history store's page cache is 32 MiB rather than redb's 1 GiB default. It did not
change the mirrored nest's idle figure on macOS, where the cold test saw 240 MB: see Measured.
Measured
All on Ethereum mainnet over Alchemy.
- Moving head against Etherscan. One address (0x9522…bafe5) over a 600-block window ending at the
head. rotki's lookup of now minus 15 seconds returned the same block as Etherscan's, andtxlist
(21 rows),txlistinternal(3) andtokentx(0) up to it were identical to Etherscan's, every
shared field equal (17 per transaction row, 14 per internal row). Three minutes later the head had
moved 18 blocks; the same queries were covered on demand and identical again. 687 RPC calls in
all, 572 of them the block-body scan of the window. - Start. 206 ms from a covered store to serving, 36 MB RSS after.
- Memory with a mirror (26 partitions, 25.9M to 26.16M, one address). On Linux (jemalloc): 5.1.1
peaked at 108 MB and idled at 36 MB, 5.2.0 peaked at 112 MB and idled at 48 MB. On macOS RSS
idled at 129 MB (5.1.1) and 140 MB (5.2.0), but 5.2.0's physical footprint was 68 MB (peak 84.5):
the rest is freed allocator regions macOS keeps resident, and a restart idles at 32 MB. The cold
test's 240 MB was this macOS figure. - At rest a poll costs one more call than before (the RPC's
finalizedtag) and one more while
hashes above the finalized head are kept.
Gated on Lodestar's data
Every change is in the address-history path, which no production nest declares, so this release changes
no query path the release gate measures.
Compatibility and upgrade
Replace the binary and restart. An address-history store written by 5.1 has no finalized block hashes,
so it cannot show that its finalized history is the chain's. At the first start it is renamed, intact,
to nuthatch.redb.pre-5.2-<unix time> beside where it was, one warning line says so and where it went,
and the nest reads its history again from start_block, keeping every account the old store watched.
Nothing is deleted: the old file can be kept, inspected or removed by hand. The rebuild costs what the
first sync did; getminedblocks, withdrawals and produced blocks on chains other than Ethereum mainnet
are unchanged.
v5.1.1
Nuthatch 5.1.1 fixes address history (RFC-0063) off Ethereum and makes its backfill workable on
providers that cap a call's block range. Nothing outside an [address_history] nest changes. There is
no storage migration.
Fixes
- Optimism before Bedrock was served as an empty history (#2023). Its RPC answers
trace_filter
there with an empty success, so a pre-Bedrock range read as an address with no transactions. It now
answersNUTHATCH_UNSUPPORTEDand is never scanned. A 5.1.0 nest on Optimism that started before
block 105,235,063 should be pointed at a fresh directory, since its stored coverage for that range is
wrong. - Timestamp to block off Ethereum (#2023).
getblocknobytimewas never answered on other chains;
it now is, from the RPC or from headers-only partitions. - Partitions for OP-stack chains (#2023).
nuthatch partitionsfailed on Optimism and Base after
Isthmus, where the header's withdrawals root is not a withdrawals commitment. Off Ethereum a
partition now carries verified headers only. - A timed-out RPC call reads as a timeout (#2023), so discovery narrows its span instead of
retrying the same range for good. - Discovery's span size (#2024). Several filters sharing one window each halved it on the same
stale refusal, and a provider whose stated limit changes from call to call (Alchemy on Optimism) had
its latest figure taken as a permanent ceiling. The window now proves a size before using it, a retry
never shrinks below what a refusal named, and the ceiling is the largest limit the provider has
stated.
Faster backfill on capped providers
Once the window has proved a size, discovery sends that many spans at once, eight at a time, and runs
both trace sides and the four log filters together (#2024). Answers are the same bytes. On Optimism,
vitalik.eth over 50,000 blocks on Alchemy went from 396-452 s and 654-1,002 trace_filter calls to
95-105 s and 245-292 calls. Gnosis, where hydration dominates, made the same 282-283 eth_getLogs
calls in the same time.
Checked
Optimism and Gnosis against Blockscout, and Base against Alchemy's transfer API, on vitalik.eth: every
action exact (Optimism 119 transactions, Gnosis 444, with their transfers and 20 of 20 timestamps
each). Base has one internal row Alchemy omits, a failed call Etherscan also lists.
Gated on Lodestar's data
Every change is in the address-history path, which no production nest declares, so this release changes
no query path the release gate measures.
Compatibility and upgrade
Replace the binary and restart. No re-index, except the Optimism case above.
v5.1.0
Nuthatch 5.1.0 adds address history (RFC-0063): a nest that watches accounts rather than contracts,
and serves their history at /api in Etherscan's shapes, so rotki can use it as an indexer. Nothing
about an existing nest changes. There is no storage migration.
This minor is a month early. 5.x cuts features monthly, and this one was brought forward for rotki,
whose founder agreed to a local indexer source on 2026-10-09 (rotki/rotki#13335). It adds and breaks
nothing, which is all the policy asks of a minor.
Address history (RFC-0063)
A nuthatch.toml with an [address_history] table and no [[contracts]] watches the accounts it
names. It runs on its own light path, one cursor beside the event pipeline, with no SQL engine and no
sealing: 31.6 MB idle, 179 ms to start, 45 MB at its peak during a backfill.
- Transactions and transfers (#2012, #2013):
txlist,tokentx,tokennfttxandtoken1155tx,
found by tracing from and to each address and by positionaleth_getLogs, each transaction
hydrated once. - Internal transactions (#2016):
txlistinternalby address and by hash, by Etherscan's rules
(value-carrying calls, creates and selfdestructs; reverted frames listed withisError"1").
--trace-rpcnames an RPC whosetrace_filtertakes a long range. - Withdrawals, produced blocks and timestamps (#2018):
txsBeaconWithdrawal,getminedblocks
with the block reward, andgetblocknobytime. - Verified block partitions (#2019):
nuthatch partitionswrites 10,000-block Parquet files of
full headers and withdrawals. A nest that names[address_history.mirror]reads them instead of one
RPC call per block, hashing every header, linking it to its parent, anchoring the range to a block
its own RPC calls finalized and recomputing every withdrawals root. A file that fails is refused,
never served. Ethereum's are published from Shanghai onward; see the hosted page on the website. - Proxy passthroughs (#2020): the
proxyactions rotki asks for andlogs/getLogs, forwarded to
the nest's RPC in Etherscan's shapes, on a loopback bind only. - MEV from the relays (#2021):
nuthatchProducedBlocksreports each block an account produced or
was paid for, with what the MEV-Boost relays say was delivered. Off unlessmev_relays = true,
because the relays see the block numbers asked about. No relay record is no figure, never zero.
A range the nest has not fetched answers NUTHATCH_INCOMPLETE, and an action it does not serve
NUTHATCH_UNSUPPORTED, so a client falls back to its next source rather than reading a gap as an
empty history. The table stays out of the nest's identity; accounts are added at runtime through
POST /api/watch, on a loopback bind.
Checked against Etherscan on vitalik.eth over Ethereum blocks 18M to 20M: 2,318 transactions, 3,334
token, 106 NFT and 53 ERC-1155 transfers, 3,131 withdrawals and 20 of 20 timestamps, every field
equal; internal transactions, block rewards and relay deliveries on pinned cases, to the wei.
scripts/rfc0063-gate.sh re-runs it.
Fixes
/readyage assertions in the serve tests allow a clock tick (#2015), and two other flakes that
turned main red are gone (#2009).
Gated on Lodestar's data
Address history runs on its own path and is reached only by a nest that declares it, so this release
changes no query path the release gate measures. Its commit carries the gate's statuses as usual.
Compatibility and upgrade
Replace the binary and restart. No re-index. A nest without [address_history] behaves exactly as on
5.0.1.
v5.0.1
Nuthatch 5.0.1 is the first published 5.x release. It is 5.0.0 with one fix to how the release is
built, so everything in the 5.0.0 notes applies to it: the WASM layer is gone, a
[screening] table no longer loads, shipped binaries use fat LTO, and new segments hash differently.
Why there is no 5.0.0 download
v5.0.0 was tagged and never published. Its aarch64 Linux build was shut down twice during the
fat-LTO link of nuthatch, out of memory on the ARM runner, and a release with a missing target
stays a draft. 5.0.1 gives the Linux release runners swap before they build, which changes nothing
the dist profile produces (#2008).
Gated on Lodestar's data
The code is 5.0.0's, which passed the release gate on every production nest (see its notes). This
release's commit carries its own release-gate/<nest> statuses from the same gate run against a
Linux dist build of it.
Compatibility and upgrade
As for 5.0.0: replace the binary and restart, after deleting any [screening] table. From 5.0.0
it is a binary swap.
v4.15.2
Nuthatch 4.15.2 changes one default: a nest started without --poll-interval now asks for the tip every
5 minutes instead of at the chain's block time. It is 4.15.1 plus that change.
A nest polls every 5 minutes unless told otherwise
Every poll is billed whether or not a block carried an event: a tip call, and when a window commits, a
reorg check, a checkpoint and a finalized probe. An idle nest on a metered endpoint spent most of its
cost polling. Until now an unset --poll-interval meant the chain's block time, never under 2 s, so a
nest polled every 2 to 12 seconds. The openloom-bsc stopgap nest, polling once a minute, costs about
$3 a month on Alchemy at rest; at 5 minutes the polling cost falls to a fifth of that.
Without the flag a caught-up cursor now waits 5 minutes, or the block time if that is longer, which no
registry chain's is. New rows can take up to 5 minutes to appear. A nest whose readers need the
tip closely passes a shorter interval, for example --poll-interval 2s, exactly as before (#1995).
/ready is unchanged: freshness.poll_interval_secs reports 300 for a defaulted interval, and
poll_interval_source stays block_time for a default and flag for an explicit one.
Gated on Lodestar's data
Every production Lodestar nest already passes --poll-interval 5m, so none of them changes behaviour.
Compatibility and upgrade
Replace the binary and restart. No re-index and no configuration change. To keep the old cadence on a
nest that relied on it, add --poll-interval with the chain's block time.
v4.15.1
Nuthatch 4.15.1 fixes one wrong answer in the GraphQL surface of the graph build: a filter on a
Bytes field with a checksummed address matched nothing. It is 4.15.0 plus that fix. A default build
behaves exactly as 4.15.0 does.
A checksummed address now matches
graph-node decodes a Bytes input as hex, so its case means nothing there, and a nest stores hex in
lower case. Compared as text, cards(where: { owner: "0xFeBD…" }) with the mixed-case address a
wallet hands an app returned an empty list instead of the owner's rows: a wrong answer with no error.
Filter values on a Bytes field, or on a reference to an entity whose id is Bytes, are now lowercased
before they reach SQL, for equality, _in lists and _contains. A singular lookup by a Bytes id is
treated the same way. String and ID values keep their case (#1992).
Found on the subgraph stopgap for openloom-bsc (nuthatch-org/graph-support#53), which has run this
fix since 2026-10-08.
Gated on Lodestar's data
The change is confined to how a GraphQL filter value is written into SQL in a graph build. No query
path of a default build, and nothing the release gate measures, is touched.
Compatibility and upgrade
Replace the binary and restart. No re-index and no configuration change.
v4.15.0
Nuthatch 4.15.0 adds the head count: after a successful nuthatch init, Nuthatch asks once whether it
may count you. The default is no, and nothing about serving, storage or indexing changes. There is no
storage migration.
The head count (RFC-0054)
Non-negotiable 3 is amended to allow exactly this and nothing else: no network call about the operator
that the operator did not explicitly turn on, with this one opt-in as the named exception (#1988).
- The question appears once per machine, after
inithas written the nest and printed its last line,
and only with a terminal on stdin and stdout. Enter is no. The answer is kept in
~/.config/nuthatch/count.toml(or under$XDG_CONFIG_HOME) and never asked again. - With a yes, each later
initsends one object tohttps://count.nuthatch-indexer.com: version, OS,
architecture, a built-in chain id (anything else is sent as 0) and which kind ofinitit was. No
identifier, no address. Everything a command sends shares one two-second ceiling, and a failure is
never printed. CIorNUTHATCH_NO_COUNTsilences it, question and pings alike.nuthatch countshows where things stand;on,off,payloadandforgetdo what they say.- The receiver keeps a tally per UTC day and nothing else. Its source is
count/receiver/, and the
totals are public at https://www.nuthatch-indexer.com/count.
Every line outside src/count.rs that reaches it is tagged, and a new leg of the required CI check
deletes the module and those lines and runs the whole suite on what is left. Deleting the head count
costs a self-hoster nothing, and that is now a job rather than a sentence.
Tooling
- The subgraph stopgap orphan alert retries a failed fetch twice before paging, honouring
Retry-After,
and a page now carries the HTTP status and the start of the body (#1990).
Gated on Lodestar's data
The head count runs only inside init, which serves nothing, so this release changes no query path the
release gate measures.
Compatibility and upgrade
Replace the binary and restart. No re-index. A scripted init without a terminal is never asked
anything and sends nothing.
v4.14.0
Nuthatch 4.14.0 adds maintained views, keeps a view's own ORDER BY, and fixes two pieces of release
tooling. There is no storage migration, and a nest that declares no maintained view behaves exactly as on
4.13.0.
Maintained views (RFC-0062)
A nest can list views in a new maintained.toml. A maintained view is evaluated exactly as a request would
evaluate it and stored under maintained/, keyed on every input it read: the binary and engine, the
authored files, the schema and sealed segments of the tables it reaches, and their hot rows. A request
reads the stored copy only when its own inputs hash to that key, and otherwise answers from the view as
before, so a copy can never be stale or differ. Copies are rebuilt after a commit or seal reaches the tip,
never on the commit or seal path, and a newer state always wins over a queued older one (#1977, #1979).
maintained.tomlis outside the data identity, so a nest still seeds from the mirror. Copies of a view
no longer declared are removed.- A view that cannot be maintained (a volatile function, an incremental entity, offchain data,
labels,
a name that is not a plain identifier) stops the nest at load and failsnuthatch check, by name. - Metrics
nuthatch_maintained_*and per-view counters on/readyappear only on a nest that declares one.
On the BetSwirl stopgap nest (160,802 bets, ThinkPad, 2026-10-07) every SDK query shape answered in
0.02 to 0.76 s from copies, against 0.9 to 3.1 s computed, with byte-identical answers, including through
reorgs 1, 3 and 5 blocks deep. Tip lag was unchanged with a build after every commit. RFC-0062's S3
(check --maintained) and S4 were not pursued.
A view's ORDER BY is kept
An ORDER BY (and LIMIT) at the end of an authored view or a derived table now orders what is read from
it, whenever nothing above reorders the rows. Every earlier release silently dropped it (#1986, #1984).
A view that is joined, aggregated, unioned or windowed plans as before. An ordered view filtered by IN,
EXISTS or a correlated subquery, or read through unnest or row_number() OVER (), is still unordered.
Tooling
- Lodestar parity now treats a collection by any collector other than GraphTallyCollector as a known
difference, which is the network subgraph's own rule, and still fails on an unexplained row (#1985, #1983). - The nightly mutation check names a baseline entry stale only if that shard actually tested it (#1974).
- The subgraph stopgap orphan alert also watches arbitrum-nova, soneium, kaia, botanix and monad-testnet (#1978).
Gated on Lodestar's data
The release gate passed on each change against v4.13.0 on the allocations nest copy: 75 of 75 answered,
none regressed, no answer differs, peak RSS inside the 2,048 MiB budget.
Compatibility and upgrade
Replace the binary and restart. No re-index. The view ORDER BY change can reorder rows a client read
from an ordered view without ordering itself; it now matches what the view says.
v4.13.0
Nuthatch 4.13.0 is an engine release: a page taken over a chain of left joins no longer computes every
row first. There is no storage migration and no other change.
A page over left joins reads only the rows it keeps
Burrmill moves to d8f79e4 (#1970, nuthatch-org/burrmill#94).
- An
ORDER BY ... LIMIT(or a plainLIMITup to 10,000 rows) over a chain of left joins now takes
its rows from the preserved side first, and every other side is narrowed to the keys those rows
carry. The rewrite stays out of inner joins, filters over joined columns, shared subqueries, NULL-matching
joins, mark joins and recursive terms. - Joins build on the narrowed side rather than whichever side merely looked smaller.
- Registering many views no longer rebuilds the name index each time (1,500 views: 31 s to under 1 s).
On the BetSwirl stopgap nest (160,802 bets, cold, 512 MB engine limit, ThinkPad, 20-run p50, 2026-10-07)
the SDK's first bets pages went from 3.0 s to 2.0 s. A deep offset (skip: 150000), a where user
filter, tokens and the single-bet query are within noise of 4.12.0, because nothing in them can be
narrowed. Answers are byte-identical. #1951
records what under 1 s would take.
Gated on Lodestar's data
The release gate ran this build against a copy of the Lodestar allocations nest with 4.12.0 as baseline:
75 of 75 statements answered, none regressed, none differed; p99 2,711 ms against 2,675 ms; peak RSS
1,923 MiB against 1,918 MiB, under the 2,048 MiB budget. Two candidates failed the gate on the way and
were fixed with tests before this one passed.
Compatibility and upgrade
Replace the binary and restart. No re-index. Process RSS on a narrowed page can be up to about 160 MB
higher than on 4.12.0, because the narrowed rows are held outside the engine pool; it stays within the
per-cursor budget on every nest gated.