# How Local Cache Pruning Works in celld to Manage Disk Space

> Discover how celld manages disk space with local cache pruning. Learn how the LRU algorithm evicts hibernation snapshots to prevent disk exhaustion and maintain optimal performance.

- Repository: [Deno/celld](https://github.com/denoland/celld)
- Tags: internals
- Published: 2026-08-10

---

**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`](https://github.com/denoland/celld/blob/main/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`](https://github.com/denoland/celld/blob/main/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`](https://github.com/denoland/celld/blob/main/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`](https://github.com/denoland/celld/blob/main/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`](https://github.com/denoland/celld/blob/main/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`](https://github.com/denoland/celld/blob/main/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:

```rust
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:

```rust
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`](https://github.com/denoland/celld/blob/main/crates/celld/replication.rs) demonstrates the final deletion loop that removes the selected hibernated files:

```rust
// 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`](https://github.com/denoland/celld/blob/main/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`](https://github.com/denoland/celld/blob/main/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`](https://github.com/denoland/celld/blob/main/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`](https://github.com/denoland/celld/blob/main/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`](https://github.com/denoland/celld/blob/main/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.