---
title: "Durability & crash safety"
description: "How generated writes stay crash-safe — a per-model write-ahead log fsynced before columns, torn-tail recovery, a single-writer directory lock, and a bounded WAL."
purpose: "orientation"
structure: "C"
---
Writes are crash-safe. If the process is killed mid-write — even with `kill -9` — you never lose a row that was already acknowledged, and the data directory never ends up corrupt. ForgeDB generates this write path per model; nothing reads your schema at runtime.
## The durable write path
Every `insert`, `update`, and `delete` first records what it is about to do in a per-model log, flushes that record to disk, and only **then** writes the data columns. Because the log lands first, a crash partway through leaves behind a record that recovery can replay.
## Recovery and torn-tail repair
On open, ForgeDB checks each model, discards any half-written row at the end of the columns, and replays the log to restore anything that was acknowledged but never reached the columns. You can run the proof yourself with `make crash-test`: it generates real database code, inserts rows, kills the process with no clean shutdown, reopens in a fresh process, and asserts every committed row survived.
## Single-writer directory lock
One process writes to a data directory at a time. A second writer that tries to open the same directory is refused rather than allowed to race. This is the v1 contract: single writer per process, per data directory.
<Callout type="warning" title="Single-writer-per-process is the v1 contract">
Concurrent writers are not handled by simply opening the directory twice — the lock refuses
that. Multi-process writes are coordinated by a separate control plane; see
[Transactions & MVCC](/docs/features/transactions-mvcc/).
</Callout>
## Bounding the WAL — checkpointing
Left alone, the log would grow forever. So ForgeDB flushes the columns to disk and then trims the log once a model has taken a set number of writes. The log sawtooths instead of growing without bound, and a crash right after a trim only has to replay the small recent tail.
## Configuring durability
Fsync is a knob, not a fixed choice:
```toml
[storage]
fsync = "always" # "always" | "never"
wal_checkpoint_interval = 1000
```
`always`, the default, flushes on every commit for full crash safety.
<Callout type="warning" title="`never` trades safety for throughput">
Setting `fsync = "never"` removes the `F_FULLFSYNC` barrier from the write path — much
faster, but a crash can lose recently-acknowledged writes. Use it only when the durability
requirement genuinely allows it.
</Callout>
See [storage configuration](/docs/config/storage/) for the full set of storage knobs, and
[Backup & restore](/docs/features/backup-restore/) for taking snapshots of a durable data
directory.
## Limits
- **One writer per process, per data directory** — enforced by the directory lock. Multi-process
writes go through a separate coordinator; see [Transactions & MVCC](/docs/features/transactions-mvcc/).
- Many-to-many link tables have no log of their own, so a crash mid-link can leave a duplicate
pair. Traversal is latest-wins and does not dedupe.
- The checkpoint interval is tunable through
[`[storage].wal_checkpoint_interval`](/docs/config/storage/); `fsync` is baked in when you
generate, not changed at runtime.