---
title: Operations
description: The flusher, manual flush and maintenance, parked boards, engine stats,
  eviction, and the destructive operations that stay off the Engine surface.
---

## Flusher

Under `writePath: "wal"`, a background flusher drains each board's pending WAL
records into the lake, off the write path. `flushIntervalMs` controls the
cadence, defaulting to 30 s. `0` disables the timer and leaves flushing entirely to
explicit calls, which is what the test suite does.

The flusher batches records to reduce the number of serialized catalog commits.

Flush lateness only costs rebuild-replay length on the next cold attach.
Durability already lives in the WAL the moment a write acks, regardless of
when the flusher gets to it.

Call `engine.flush(boardId?)` to drain on demand, one board, or every board
with pending WAL records when called with no argument:

```ts
const stats = await engine.flush("acme_roadmap");
stats.unwrap("flush failed"); // FlushStats[]: from, to, records, skipped
```

Under `writePath: "lake"` every write already commits to the lake synchronously,
so `flush` is a no-op that returns an empty list.

## Maintenance

`engine.maintain(olderThanHours?)` runs lake maintenance now: snapshot expiry,
file cleanup, and small-file merge. The flusher also runs this on its own clock,
once an hour, so under `writePath: "wal"` you normally never call it yourself.
It exists for a `writePath: "lake"` deployment (which has no flusher) to wire
into its own rotation, or for forcing an off-cycle cleanup:

```ts
await engine.maintain(0); // expire snapshots older than 0 hours -- i.e., all of them
```

## Parked boards

A board the flusher fails to flush repeatedly gets parked rather than retried
in a tight loop against the same failure: backoff starts at 30 s and doubles up
to a 10-minute cap.

Parking never touches durability: the board's WAL records sit untouched,
since durability never depended on flushing. They do accumulate, though, so a
parked board is surfaced rather than swallowed. `engine.stats().flusherParked`
and the `node.stats` wire procedure both carry it, as a list of
`{ boardId, failures, since }`. One poisoned board parking does not stop any
other board's flush from proceeding on the same pass.

Recovery is fixing whatever made the lake-side apply fail: a dropped table, a
permissions change, anything that turns a normal write into a thrown error.
Then either wait for the backoff to elapse, or call `engine.flush(boardId)`
directly: an explicit call always retries regardless of the parked state,
which is the manual unpark.

## Reading EngineStats

`engine.stats()` (and the `node.stats` wire procedure, which adds `nodeId` and
the list of currently-cached `boards`) reports:

- `cached` / `capacity`: attached replica files against the configured ceiling
- `hits` / `rebuilds` / `reattaches`: how requests found their replica,
  already-attached, rebuilt from the lake, or reattached from an existing file
- `evictions`, `connectionsInUse`, `flusherParked`

What healthy looks like:

- traffic dominated by `hits`, with `rebuilds`/`reattaches` showing up only on
  genuinely cold boards or after a restart
- `connectionsInUse` well below `poolSize`, not sitting persistently near it
- `flusherParked` empty, or clearing within a backoff cycle or two

Investigate sustained pool saturation or boards that remain parked.

## Eviction

`engine.evict(boardId, deleteFile = false)` drops a replica from the
attached-file cache. The file stays on disk by default; `deleteFile: true` also
removes it, which is how a test simulates losing the node's local disk entirely
rather than just its warm cache.

Eviction waits out in-flight work on that board before it lets go: it will not
detach a file a concurrent request is still using. The cache converges
_after_ the call returns, not necessarily inside the same tick.

Do not assert `cachedBoards()` or `stats().cached` immediately after an evict
(or a `dropBoard`, which evicts internally) in a test or a script; poll, or
accept that convergence is asynchronous.

## Replica index upkeep

The engine automatically rebuilds replica ART indexes to reclaim space after updates and deletes. Rebuilds are expected on frequently updated boards and require no operator action.

## Compacting after upsert-heavy load

`upsert` writes a new version of each row rather than issuing an `UPDATE`, so
repeated upserts to the same ids leave superseded versions behind.
`engine.compact(boardId)` collapses them:

```ts
await engine.compact("acme_roadmap");
```

Run it after a board has taken a burst of upsert traffic, or on a schedule if
upserts are its normal write shape.

## Destructive: resetLake and dropCatalogSchema

:::danger[Not on the Engine]
`resetLake` and `dropCatalogSchema` live
in `lakefront`'s core module but are not methods on `Engine`, reachable only
by importing them directly, so a server node has no code path that can drop a
customer's lake. They exist for test fixtures, examples, and intentional resets,
not for anything a running node does to itself.

Deleting the data directory alone is **not** a reset. With data inlining on,
recently-written rows live as rows in the catalog Postgres rather than as
Parquet files. Wiping only the directory leaves the data intact in the
catalog, and the next boot inherits the previous run's board.

`resetLake({ postgres, data, namespace? })` drops the catalog schema _and_
removes the data directory; do both, or use it.

Lakefront is pre-1.0, and its catalog layout changes between versions without a
migration path: schema DDL uses `CREATE TABLE IF NOT EXISTS`, so a table that
already exists from an older version is left as-is rather than gained new
columns.

A fresh lake created against an old catalog schema will not pick up a shape
change on its own: a `resetLake` is sometimes what an upgrade requires. Check
the changelog for the version you're moving to.
:::
