How Local Cache Pruning Works in celld to Manage Disk Space

celld prevents disk exhaustion by periodically evicting least-recently-used hibernation snapshots when the total cache size exceeds a configurable byte limit, using an LRU algorithm implemented across the scheduler, runtime façade, and replication layers.

The celld node in the Deno ecosystem preserves hibernation snapshots for every cell it hosts, storing them as SQLite files with the .hibernated extension under the node's watch directory. Without intervention, these files accumulate indefinitely and can exhaust available disk space. Local cache pruning solves this by enforcing a user-defined size ceiling through an automated eviction process that removes the oldest snapshots first.

The Pruning Pipeline

The local cache pruning mechanism operates through a five-stage pipeline that spans multiple source files. Each stage handles a specific responsibility, from timer-based scheduling to physical file deletion.

1. Timer-Based Scheduling in the Main Loop

The pruning process begins in crates/celld/main.rs, where a Tokio interval drives the operation. The scheduler creates a tokio::time::interval_at timer using the LOCAL_CACHE_PRUNE_PERIOD constant. When the interval fires, the system checks whether a replication backend exists and whether a local_cache_max_bytes limit is configured. If both conditions are met, it spawns a blocking task to execute the pruning logic without stalling the async runtime.

According to the source code at lines 50–78, the main select! block handles the tick event by calling replication.prune_local_cache(max_bytes) inside tokio::task::spawn_blocking. The resulting tuple—containing counts of kept files, evicted files, and total bytes used—is then logged for observability.

2. Runtime Interface

The Runtime::prune_local_cache method in crates/celld/runtime.rs (lines 59–66) serves as the public API façade. This method forwards the byte limit to the underlying replication backend, typically an LtxRepl instance. By abstracting the replication details behind the runtime interface, celld allows the main scheduler to remain agnostic about which specific storage backend performs the actual cleanup.

3. Directory Traversal and File Discovery

The heavy lifting begins in crates/celld/replication.rs within the prune_watch function (lines 76–119). This implementation recursively walks the entire watch directory tree, identifying every file bearing the .hibernated extension. For each match, it extracts two critical metadata points: the file size and the last_used_ms timestamp derived from the file's last-modified time. These entries are collected into a vector for the eviction planner.

The function specifically targets files matching the pattern <watch>/<cell>/ltx/e<epoch>/db.sqlite.hibernated, ensuring it only considers valid hibernation snapshots and ignores active databases or unrelated files.

4. LRU Eviction Policy

The eviction decision logic resides in crates/logic/cache.rs inside the plan_eviction function (lines 26–48). This implementation uses a least-recently-used (LRU) algorithm: it sorts the collected file entries by last_used_ms in descending order (most recent first), then calculates a running total. Starting from the oldest entries at the tail of the list, it marks files for deletion until the cumulative size of the remaining (kept) entries falls below the max_bytes threshold.

This approach guarantees that the most recently accessed snapshots survive while older, stale snapshots are selected for removal.

5. Physical Removal and Metrics

Returning to crates/celld/replication.rs, the system iterates over the indices returned by plan_eviction and deletes the corresponding files using std::fs::remove_file. After deletion completes, control returns to the main loop in main.rs, which logs the final metrics: the number of snapshots kept, the number evicted, and the total byte count currently occupying the cache.

Configuration and Safety Mechanisms

You configure the cache ceiling using the --local-cache-max-bytes command-line flag or the CELLD_LOCAL_CACHE_MAX_BYTES environment variable. If you omit this setting, the pruning task skips execution entirely, allowing the cache to grow without bound.

The pruning logic includes a critical safety guard: it never deletes the last local copy of a cell that the bucket cannot restore. Before a cell enters hibernation, the system checks the epoch_replicated status elsewhere in the codebase. Pruning only removes extra snapshots that have already been safely persisted to remote storage, preventing data loss during aggressive cleanup operations.

Practical Code Examples

To start celld with a 2 GiB local cache limit, configure the runtime options as follows:

let options = RuntimeOptions {
    // ... other options ...
    local_cache_max_bytes: Some(2 * 1024 * 1024 * 1024), // 2 GiB
    ..Default::default()
};
let runtime = Runtime::start(options)?;

For administrative scripts or testing scenarios, you can manually trigger pruning and inspect the results:

let (kept, evicted, bytes) = runtime.prune_local_cache(500_000_000);
println!("after pruning: {kept} kept, {evicted} evicted, {bytes} bytes used");

The following snippet from crates/celld/replication.rs demonstrates the final deletion loop that removes the selected hibernated files:

// Inside `prune_watch` – the core eviction loop
for &index in &evict {
    // Delete the selected hibernated file
    let _ = std::fs::remove_file(&paths[index]);
}

Summary

  • celld stores cell snapshots as .hibernated SQLite files under the watch directory that can grow indefinitely without intervention.
  • A periodic timer in main.rs triggers pruning at intervals defined by LOCAL_CACHE_PRUNE_PERIOD, spawning the work on a blocking thread to keep the async runtime responsive.
  • The LRU algorithm in cache.rs sorts snapshots by last-used time and evicts from the tail until the total size fits under local_cache_max_bytes.
  • Safety checks ensure pruning never removes the only copy of unreplicated data; it only targets snapshots already backed up to remote storage.
  • Configuration occurs via the --local-cache-max-bytes flag or CELLD_LOCAL_CACHE_MAX_BYTES environment variable.

Frequently Asked Questions

What happens if I don't set a local cache maximum size?

If you omit the --local-cache-max-bytes flag and the CELLD_LOCAL_CACHE_MAX_BYTES environment variable, celld disables the pruning task entirely. The node will continue accumulating hibernation snapshots for every cell it has ever hosted, potentially consuming all available disk space on the host.

How does celld determine which snapshots to delete?

The system implements a least-recently-used (LRU) eviction policy. In crates/logic/cache.rs, the plan_eviction function sorts all discovered .hibernated files by their last_used_ms timestamp, keeps the most recently accessed files, and removes older files from the tail of the sorted list until the cache fits within the configured byte limit.

Is the pruning process blocking or non-blocking?

The pruning work runs inside tokio::task::spawn_blocking as implemented in crates/celld/main.rs. This design keeps the directory traversal, metadata collection, and file deletion operations off the main async runtime thread, ensuring that celld continues handling requests and replication tasks without latency spikes during cleanup.

Can pruning delete data that isn't backed up elsewhere?

No. The pruning logic in crates/celld/replication.rs only targets snapshots that are already safely persisted. Before a cell enters hibernation, celld verifies the epoch_replicated status to ensure the data exists in remote storage. The local cache pruning mechanism specifically removes extra local copies, never the sole copy of unreplicated data.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →