When your database says your transaction is saved, your row usually isn't in the table file yet. Pull the power cord in the middle of a write, and you still lose zero committed rows.
That's not luck. It's one file doing one job: the write-ahead log (WAL).
Prefer to watch? Full walkthrough with the crash-and-replay animation:
The obvious way to save data — and why it fails
The obvious design: you run an UPDATE, the database finds the right spot in the table file, and overwrites the value there. Simple. It has two problems.
- It's slow. One small change can touch the table and two or three indexes — pages scattered all over the disk. Every one of them would have to be forced to disk before the database could say "saved."
- It's unsafe. If the power dies halfway through, the table might have the new value while the index still has the old one. Now the database disagrees with itself, and nothing tells you.
Write it down first
So databases do something that sounds backwards: before they change the table, they write down what they're about to change. One short record, added to the end of a log file — "account 1, balance minus 100."
That's the write-ahead part: the log is always written ahead of the data. The PostgreSQL docs put the rule plainly: changes to data files may only be written after the log records describing them have reached permanent storage.
Think of a waiter with a notepad. They don't run to the kitchen after every word — they write the order down first. If they trip and drop the plates, the order is still on the pad, and the kitchen just cooks it again.
One transfer, step by step
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
- Each
UPDATEchanges a page in memory and adds a record to the log. - On
COMMIT, the database forces only the log to disk. - As soon as that's safe, you get your "OK."
- The changed table pages are still in memory. They'll reach the disk later, in the background.
Why it's also fast
The log only grows at the end, so writing it is one straight, sequential write. The table pages are scattered across the disk. So you pay for one cheap write at commit time and push the expensive, scattered writes to later. The Postgres docs make the same point: because the WAL is written sequentially, syncing it costs much less than flushing the data pages.
Pull the plug: crash recovery
Right after a commit, your saved data lives in two places: the log (on disk) and the updated table pages (in memory). Now the power dies.
- Everything in memory is gone.
- The table file on disk is old — it still says the money never moved.
- The database starts up again, opens the log, and replays it, record by record: minus 100, plus 100, commit.
- The table catches up. Zero committed rows lost.
Postgres calls this roll-forward recovery, or REDO. Crash recovery is just the database reading its own notes.
What about a transaction that never committed?
Say the power dies between the two updates. There's no commit record in the log for that transaction, so after the restart its changes count as if they never happened. You never get half a transfer — all of it, or none of it.
Checkpoints: why the log doesn't grow forever
If every change goes into the log, doesn't the log grow forever? No. Every so often the database runs a checkpoint: it writes all the changed pages from memory into the table files. Everything before that point is now safely in the table, so the old log can be recycled.
In Postgres, a checkpoint starts every 5 minutes by default, or sooner if the log is about to pass 1 GB (checkpoint_timeout and max_wal_size). It's also why recovery is fast: it only replays from the last checkpoint, not from the beginning of time.
The torn page
One nasty detail almost breaks all of this. A Postgres page is 8 kB. If the power dies while that page is being written, the disk can end up with a torn page — half new data, half old. A small log record describing one row change can't repair a page that's already broken.
So Postgres does something clever: the first time a page changes after a checkpoint, it writes a full copy of that page into the log (full_page_writes, on by default). If recovery finds a torn page, it starts from that clean copy and replays the rest on top.
One log, many jobs
The log isn't just for crashes.
- Replication. A replica is just another server replaying the same log. Postgres streaming replication ships these WAL records over the network. A replica that's "behind" is simply at an older position in the log.
- Positions. Every record has a position called an LSN (log sequence number). You can ask Postgres for the current one:
SELECT pg_current_wal_lsn();
- Other databases. It's not a Postgres quirk. MySQL's InnoDB has a redo log, and SQLite has a WAL mode. Same idea everywhere: write it down first.
What this means for your code: every commit waits for the disk
Even if you never build a database, this changes how you write to one. Every commit waits for the log to reach the disk — once per commit.
-- autocommit: one commit per row
INSERT INTO events (payload) VALUES ('a'); -- × 10,000
-- one transaction: one commit
BEGIN;
INSERT INTO events (payload) VALUES ('a'); -- × 10,000
COMMIT; -- one wait
Insert 10,000 rows with one commit each and you wait for the disk 10,000 times. Wrap them in one transaction and you wait once. Same rows, same table. If your bulk import is slow, check your commits before you blame the inserts.
The setting that forgets
Because that wait is the slow part of every commit, Postgres lets you skip it:
SET synchronous_commit = off;
Your commit now returns before the log is on disk. It's faster — but if the server crashes, the most recent "saved" transactions can disappear. According to the docs, the risk window is at most three times wal_writer_delay (200 ms by default, so roughly the last 0.6 seconds).
Here's the difference that matters:
-
synchronous_commit = offrisks losing the last few transactions, but what's left stays consistent. The docs call it data loss, not data corruption. -
fsync = offmeans a system crash can corrupt the whole database.
One is a trade-off you can make on purpose, for data you can afford to lose. The other is a trap.
The takeaway
"Saved" doesn't mean it's in the table. It means it's in the log — and the log is enough to rebuild everything else.
What's the riskiest durability setting you've ever turned off, just to go faster? Tell me in the comments — I read them.
I make Vlad's Stack — how the tools you use every day actually work, for people who write code. The full video walkthrough is above; it's part of a short series on how databases work, next to how an index finds one row in a million.
Top comments (0)