Has GC Finished Its Job If It Stops Safely?
Proving a historical state requires keeping data from that time. Keeping all verification data indefinitely, however, makes storage harder to maintain as the ledger processes more transactions. Once we have decided what to retain, we also need a way to clean up what is no longer needed.
Historical state and proofs distinguished an answer saying that data cannot be provided from one saying that a record was absent at the time. This article asks about deleting that data. If cleanup protects everything that must survive, has it done its job?
nigo-protocol's local tests on September 18, 2026 produced two answers. While transactions kept arriving, the conditions for deletion changed and GC stopped safely. Yet it completed no cleanup cycles during those periods of load. We need to read that result for both what it protected and what it could not yet accomplish.
Old data is not necessarily unused data
GC, or garbage collection, finds and reclaims data that is no longer needed. The nigo-protocol GC discussed here cleans up Merkle tree nodes used to verify state. Here, a node means one stored piece of the tree, not a computer participating in the network.
A Merkle tree combines hashes of state data. Its state root, at the top of the tree, summarizes the state at a particular point. The data beneath it forms paths for checking whether individual records belong to that state. When a state change produces a new root, multiple roots can still share the parts that did not change.
Suppose we retain the states at heights 10 and 20. Height is a block's position in the sequence, and each state
is the result after that block has been processed. To illustrate the relationship, consider just part of the data
reachable from the two roots. Branch X is used only by height 10, while branch S is shared by both states.
Assume no other retained state uses X.
Now suppose we remove height 10 from the set of states to retain.
A simplified view of the retention relationships. X and S are tree branches, not the original asset or state data; other branches are omitted. Although height 10 is no longer retained, its data has not yet been deleted.
In this case, X becomes a deletion candidate because no remaining retained root can reach it.
But S is still needed to verify height 20. Deleting it simply because it was stored when height 10 was created
would also damage proofs for height 20, which we have chosen to keep.
The deletion criterion is therefore whether the node is unreachable from every root currently protected. Starting from the roots to retain, GC marks the nodes that are needed and leaves the unused ones as candidates. This check sits between the decision to stop retaining an old root and the act of deleting data once connected to it.
The scope matters, too. This GC does not delete original StateCell data, the records holding assets and state, or the bodies of blocks, transactions, and execution results. It does not delete an old Cell's original data simply because that Cell has been consumed from the current state. Reclaimed quantities in this article count verification tree nodes, not bytes returned to the filesystem.
The criteria can change after candidates have been found
Suppose GC has recorded X as a deletion candidate in the example above. No deletion has taken place yet.
What happens if block 21 is finalized and its new state is stored in the meantime?
The set of roots to retain and the stored tree nodes may now differ from what the diagnostic examined.
We need to check whether a conclusion reached at height 20 still applies at height 21.
New blocks are not the only events that change protection conditions. nigo-protocol has pins that keep particular currently retained states protected, as well as protection for data being read by an ongoing query. Adding such protection or changing its expiry must also be reflected in the deletion decision. A pin is not used to bring back a state that has already fallen outside retention.
A new block does not mean that X, or every other candidate, has actually become necessary again.
It means the basis for applying the earlier diagnostic to the current storage has changed.
nigo-protocol records both the retention criteria and a generation counter tracking storage changes when it runs the diagnostic.
Before deletion, it checks those references again. If they are no longer valid, it marks the job STALE.
That means the earlier decision is outdated; it does not grant permission to delete the candidates anyway.
A completed diagnostic is therefore not, by itself, permission to delete. A grace period cannot provide that permission either. If protection conditions change while GC waits, elapsed time alone cannot justify proceeding.
Automatic execution follows the same deletion checks
nigo-protocol's automatic GC runs on H2 and RocksDB persistent storage and is disabled by default. When enabled, it periodically creates and advances jobs through the existing GC engine. Each cycle covers diagnosis, deletion, and cleanup of the job's temporary data, followed by a wait for the next cycle.
Inside that process, GC checks the data reachable from retained roots and checks protection conditions again just before deletion. It deletes in small batches. Each batch's deletions and progress update are committed atomically: no intermediate result is left in which only part of that operation was saved. GC also checks the retained tree's connections before and after deletion.
If the criteria change, the old job is cleaned up and a new diagnostic is attempted after a wait. Retries are bounded. When successive attempts keep finding changed criteria and reach the configured limit, the automatic policy suspends and requires operator action. It does not stop consensus or skip protection checks to raise the number of completed cycles.
There is another important distinction here. Some deletions may already be committed even when the cycle never finishes. An earlier batch may have been deleted under valid conditions, followed by a change before the next batch can run. A later suspension does not roll back those earlier, safely committed deletions.
The metrics therefore mean different things. The deleted-node count measures individual deletions already committed. The completed-cycle count measures cycles that have finished deletion and cleaned up that job's temporary data. One count can increase while the other stays unchanged.
Cleanup did not finish while transactions kept arriving
To examine this difference under load, the September 18 test ran three times each on H2 and RocksDB. Each run used a fresh database and submitted real signed transactions, checking their execution results and inclusion in blocks. Query requests ran concurrently during the phases with automatic GC enabled.
Writes followed two patterns: continuous submission, and paced submission that paused for two seconds after submitting and confirming each batch of up to eight transactions. Two backends, three runs each, and two write patterns produced 12 observed periods under load. Every period satisfied both a minimum duration of 30 seconds and a minimum of 128 actual execution results. These checks used a local consensus mode on one node; they do not describe a production deployment with multiple nodes.
| Write pattern | Completed-cycle increase | Logical node deletions |
|---|---|---|
| Continuous · 6 periods | 0 in every period | None |
| Paced · 6 periods | 0 in every period | Some progress on H2 only |
Completion means finishing a cleanup cycle. Changes in completed cycles and deletions were observed from the first confirmed transaction batch through the last. Every period under load ended with the automatic policy suspended at its retry limit.
The three paced H2 periods recorded 7,000, 17,000, and 8,000 logical node deletions, respectively. Even so, their completed-cycle increases were all zero. The measurement kept opportunities for partial deletion separate from the result of finishing a whole cycle.
In all 12 periods, changing retention criteria repeatedly invalidated the diagnostics.
The automatic policy eventually reached STALE_RETRY_LIMIT and suspended.
While GC recalculated, the ledger advanced to its next state.
The current conservative checks could safely reject a job, but completing cleanup amid continuing writes
proved to be a separate problem.
The report's overall verdict was a pass. That covered bounded, safe suspension under the specified local load, transaction and query latency budgets, and the cleanup and retention checks after the explicit handoff described below. It did not mean automatic completion had been observed under load. Stopping while preserving safety conditions is necessary evidence, but it does not also demonstrate automatic cleanup of data that keeps accumulating.
Test conditions and limits of the comparison
The machine used an Apple M4 with 10 logical CPUs and 32 GiB RAM, running darwin arm64. With Java 21.0.7, the node used a 2,048 MiB heap, four active processors, four signature verification workers, and eight database connections. The test used single-node INSTANT consensus, parallel execution OFF, and zero fee amounts.
Each database was seeded with 1,032 finalized transactions splitting coins across eight owners. The configuration retained the eight most recent roots, with a separate retention pin and identifiable unused tree nodes. GC used batches of 1,000 nodes, a 100,000-step limit, a 120-second execution budget, a one-second grace period, and a limit of eight consecutive STALE cycles. The automatic interval was configured to one second under load and ten seconds during the phase without writes. These were test settings, not changes to the product defaults.
On each database, the automatic-GC-OFF baseline ran first, followed by ON over the accumulated history in that same database. Concurrent query load was also present only during ON. This was not a comparison that changed only the GC switch while starting from identical state, so it does not isolate GC overhead or establish a general performance ranking between the two backends. This article reviews the recorded results; it does not report a new measurement.
Quieter writes did not, by themselves, restart GC
Did suspended GC automatically pick up the work once the load fell? Reading the later results that way would omit an essential procedure.
After the retry limit is reached, the automatic policy remains in a state requiring operator attention. It does not clear that suspension merely because the load has dropped. A restarted policy does not take over a leftover job on its own, either.
In the test, an operator explicitly cleaned up the remaining job, then shut down the node normally and restarted it. The test then checked whether a new automatic cycle could finish without new transaction submissions. The suspended job did not resume by itself, and reduced load alone did not produce the recovery.
The sequence in the final September 18 test. It does not show unattended recovery with the operator step omitted.
Each of the six databases contained 4,096 seeded tree nodes that no retained root used. All of these were gone at the final check. Some were reclaimed in earlier phases; the rest were reclaimed during the final phase without writes. This sample of unused nodes is a different set from the total logical node deletions observed above.
After cleanup, the test checked all current Cells and their corresponding historical original data, proofs for selected historical and current states, pins, and retained coverage. Those checks passed. It did not exhaustively check every historical Cell version, but it did establish a local path that finished cleanup while preserving the protected data covered by those checks.
Read together, the results are clear. Under these conditions, GC stopped safely and finished reclamation in a quiet phase after an explicit handoff. Completion during sustained writes and unattended recovery remain unverified.
The next checks must go beyond deletion counts
Simply raising the retry limit cannot settle the remaining problem. If each new diagnostic becomes outdated for the same reason while the ledger keeps changing, a larger limit may only extend the wait without completing cleanup. Weakening protection checks to force completion would put the historical proofs we meant to preserve at risk.
The next validation needs to show repeated completed cycles and actual reclamation while transactions continue to finalize, without an operator cleaning up jobs or restarting the node. Beyond a single success, it must show whether reclamation keeps pace with newly accumulated cleanup work. It also needs to check whether progress resumes automatically as load drops, and whether transaction finalization and queries stay within their specified latency budgets throughout.
Even an increase in completed cycles does not mean physical disk usage immediately shrinks. Reusing space deleted logically inside a database or reorganizing its files is another layer of work. Judging whether total storage remains manageable requires separate checks of tree-node GC progress, retention of Cell and block bodies, and actual disk usage.
Returning to the small example, the first responsibility is to protect S, which height 20 still uses.
The second is to reclaim X, which no retained state uses anymore, while the ledger continues to move forward.
nigo-protocol's current results show suspension in service of the first responsibility and completion after an explicit handoff.
Fulfilling the second responsibility under sustained load remains work for the next design and validation.
That is why an indicator saying automatic GC is enabled is not enough. Operators also need to see when cleanup last finished, how much it reclaimed, and what a suspended process is waiting for. Observing the protection decision separately from the ability to finish the work makes a safe stop a useful starting point for the next improvement.