# Compaction and collection

> Merging small segments and reclaiming superseded blocks, without blocking anything.

Writes append. A table written in many small batches ends up as many small segments, and every
update or delete leaves the blocks it superseded on disk until something reclaims them. Two
background jobs handle both, and both are stepped and resumable — a browser tab can be
backgrounded, throttled, or closed mid-job.

## Compaction

Merges small segments into larger ones, which is what keeps a scan from paying per-segment
overhead after a long run of small writes.

```ts
await db.compactTable("orders");
```

It runs automatically by default. To drive it yourself — a slice per idle callback, so it never
holds the main thread:

```ts
const db = new MinnowDatabase(store, { autoCompact: false });

requestIdleCallback(async function step() {
  const progress = await db.compactTableStep("orders", { maxBlocks: 64 });
  if (progress.result === null) requestIdleCallback(step);
});
```

Each step processes at most `maxBlocks` output blocks and checkpoints; `progress.result` stays
`null` until the job publishes.

### Deletes and updates before compaction

Compaction is not what makes a mutated table readable at speed. A query applies the table's
deltas over its appended data directly, so a table that has been deleted from or updated answers
from the same scan every other table gets, plus the cost of the deltas themselves. What
compaction adds is returning the table to a plain append — the deltas stop being re-read on every
query, and the storage they occupy is freed.

One limit worth knowing: merging a keyed table plans the merge in memory, and a large table can
need more than the default 32 MiB budget. Raise it with `memoryBudgetBytes` when a compaction
reports `CompactionMemoryBudgetError`:

```ts
await db.compactTable("orders", { memoryBudgetBytes: 256 * 1024 * 1024 });
```

Automatic compaction backs off a table whose attempt fails, rather than retrying it on every
query.

Jobs are records in the store, so `listCompactionJobs()` finds one a previous session left behind
and `resumeCompactionJob(jobId)` picks it up. `cancelCompactionJob(jobId)` stops one cleanly —
compaction is visible-data-neutral by construction, so cancelling it can never lose data.

## Garbage collection

Reclaims blocks no live version references any more:

```ts
await db.collectGarbage();
```

A block is only collectable when no manifest, no open reader lease, and no non-terminal job still
roots it. That is what makes it safe to run while a report is being read: an open
[snapshot scope](/docs/engine/transactions.md#stable-reads) pins its version, and collection skips
everything that version needs.

The same stepped shape applies:

```ts
await db.collectGarbageStep({ jobId, maxItems: 128 });
```

## Scheduling

Neither job needs to run on a timer. Reasonable triggers:

- After a bulk load, compact the tables it touched.
- On an idle callback, take one step of whatever is outstanding.
- At startup, resume jobs a previous session left and call
  [`cleanupQuerySpill()`](/docs/engine/memory.md#spill-cleanup).

Leaving them undone costs storage and scan speed. It never costs correctness — a database that
is never compacted or collected still answers every query correctly, just over more blocks and
more bytes than it needs.

---

Minnow 0.1.0 · this page on the site: /docs/storage/maintenance/
