Write-Ahead Log (WAL)
The Write-Ahead Log (WAL) is the durability mechanism at the heart of every transactional database. Before any change is applied to the data files, a record of that change is appended to the WAL and fsynced. If the database crashes, the WAL is replayed on restart to bring the data files back to a consistent state — committed transactions survive, uncommitted ones don't. The WAL also powers streaming replication, point-in-time recovery, and transactional outboxes.
How it works
The Write-Ahead Log is, in concept, beautifully simple: before any change is made to the data files, write a record describing the change to a separate append-only log, and fsync that log. On crash, replay the log to reconstruct the data files.
This solves a fundamental storage problem: data files are large, structured, and randomly accessed. Writing a small update to a 1TB file is expensive — you have to seek to the right page, modify it, and write it back, ideally atomically. But random I/O with crash safety is hard.
The WAL turns this into sequential I/O, which is dramatically faster and crash-safe:
- Transaction begins; the engine stages changes in memory.
- As changes are made, they're appended to the WAL (an append-only file).
- On COMMIT, the WAL is fsynced — the changes are now durable on disk.
- The data files are updated later, in the background ('checkpointing').
- If a crash happens, the WAL is replayed on restart to bring the data files up to date.
The key insight: durability is guaranteed by the WAL, not by the data files. The data files can lag behind the WAL — that's fine, because the WAL is the source of truth that can always be replayed. As long as the WAL is fsynced before COMMIT returns, the transaction is durable.
The single most important setting in any transactional database is the fsync policy on the WAL. The guarantee chain is:
- The application calls
COMMIT. - The database appends the commit record to the WAL.
- The database calls
fsync()on the WAL file. - The kernel flushes the WAL to physical storage.
- Only after fsync returns does the database tell the application 'committed.'
If fsync is honored, the transaction is durable — a power loss immediately after COMMIT returns will not lose the transaction. The WAL is on disk; on restart, it's replayed and the data files are brought up to date.
If fsync is disabled (e.g., synchronous_commit = off in PostgreSQL), COMMIT can return before the WAL is on disk. This is dramatically faster but trades durability for speed: a power loss can lose the last few committed transactions. Acceptable for some workloads (analytics, caches) — unacceptable for others (payments).
Related settings that affect durability:
wal_level(PostgreSQL) — minimal, replica, or logical. Determines how much info is in the WAL (more for replication, even more for logical decoding).synchronous_standby_names— if set, COMMIT waits for at least one synchronous standby to confirm receipt of the WAL. Provides durability across machines.full_page_writes— writes the full page image on first modification after a checkpoint, defending against partial page tears. Slightly slower but safer.
The lesson: tune the fsync / synchronous_commit knobs deliberately, knowing exactly which guarantee you're trading for speed.
Once the WAL exists, a cascade of features fall out for free: (1) Streaming replication — a standby connects to the primary, receives WAL records over a socket, and replays them locally. Eventual consistency between primary and replica, with sub-second lag typically. (2) Point-in-time recovery (PITR) — keep WAL archives; restore a base backup and replay WAL up to a chosen timestamp. Recover from DROP TABLE mistakes. (3) Change Data Capture (CDC) — Debezium reads the WAL (or binlog in MySQL) and publishes every change as an event to Kafka, without placing load on the primary. This is the foundation of the outbox pattern, event sourcing, and CQRS read-model updates.
If every change is in the WAL, why do we need data files at all? Because the WAL is optimized for appends, not for queries. To answer SELECT * FROM users WHERE id = 42, you need an indexed, structured data file — not a sequential log.
The bridge between WAL and data files is the checkpoint:
- Periodically (every few minutes by default), the database writes all dirty buffers from shared_buffers to the data files.
- It records the WAL position (LSN — Log Sequence Number) at which the checkpoint occurred.
- WAL records older than the checkpoint LSN are no longer needed for crash recovery and can be recycled.
On crash recovery:
- The database reads the last checkpoint record from the WAL.
- It replays the WAL forward from that point, applying each change to the data files.
- Uncommitted transactions (those without a commit record) are rolled back via the undo log (in PostgreSQL, the abort is recorded in the WAL itself).
The checkpoint bounds recovery time: you only need to replay WAL from the last checkpoint. If checkpoints are far apart, recovery takes longer; if too frequent, they slow down normal operation. PostgreSQL auto-tunes this with checkpoint_timeout (default 5 minutes) and max_wal_size.
This is also why pg_start_backup() and pg_stop_backup() for backups interact with the WAL — they create a checkpoint and ensure WAL archiving captures everything needed to make the backup consistent.
Different databases call the WAL by different names, but the concept is identical:
- PostgreSQL —
pg_wal/(formerlypg_xlog/). Records every change.wal_level=logicalenables logical decoding for CDC. - MySQL/InnoDB — separate
redo log(InnoDB's WAL) andbinlog(MySQL server-level, used for replication). They're different logs and reconciled by an internal XA. - Oracle —
redo logfiles, in groups for rotation. Archive logs (saved redo logs) enable PITR. - SQL Server — transaction log (.ldf file).
- Cassandra — CommitLog, similar concept; SSTables are the equivalent of data files. Same LSM pattern.
- RocksDB / LevelDB — also use a WAL, similar to Cassandra.
The architectural pattern is universal: sequential log for durability, structured files for queries, background process to bridge them. Whether you're debugging PostgreSQL recovery, MySQL replication, or Cassandra compaction, the underlying mechanism is the same.
This is also why the WAL is the foundation of the transactional outbox pattern: write your business data AND an event-to-publish row in the same transaction (so they're in the same WAL fsync), then a separate process reads the outbox table and publishes events to Kafka. Because the outbox write is in the same WAL as the business write, either both survive or neither does — atomicity across the database and the message queue, without 2PC.
Why does the database fsync the WAL but not the data files on COMMIT?
Pick one answer.
Your PostgreSQL primary crashes and restarts. The last checkpoint was 5 minutes ago. What happens to transactions committed in the last 5 minutes? To transactions that were in-progress at crash time?
Pick one answer.
What's the trade-off when you set `synchronous_commit = off` in PostgreSQL?
Pick one answer.
How does the WAL enable the transactional outbox pattern for reliable event publishing?
Pick one answer.
Engineering mental model
Mental model. Think of Write-Ahead Log (WAL) as a deliberate boundary in a system. The boundary exists because something becomes harder to manage when everything is done in one place: latency, scale, failure isolation, consistency, cost, or team ownership. The useful question is not “what does Write-Ahead Log (WAL) mean?” but “what pressure makes this boundary worth introducing, and what new failure mode does it create?”
Before choosing Write-Ahead Log (WAL), name the workload, the critical user path, the dominant bottleneck, the failure you are trying to absorb, and the trade-off you are willing to accept. If you cannot name those five things, the design is probably premature.
// Pseudocode
request = receive()
result = wal(request)
return result
// Production questions:
// 1. What happens on timeout?
// 2. Can this operation be retried safely?
// 3. What is the bottleneck?Back-of-the-envelope reasoning
Numerical lens: write down traffic, payload size, read/write ratio, peak multiplier and durability target before choosing a component. The numbers should justify the architecture.
Interactive thought experiment: Write-Ahead Log (WAL)
Change the variables below and predict what breaks first in Write-Ahead Log (WAL). The production lab can later reuse these same inputs.
Change one variable at a time. Predict the failure mode first, then move the slider and see whether your mental model matches the simplified system response.
If you are stuck on Write-Ahead Log (WAL), start by drawing the request path and marking every network hop, stateful component, queue, cache and failure boundary. Then estimate where the system will saturate.
You increase traffic by 10× in a system using Write-Ahead Log (WAL). What should you inspect first?
Pick one answer.
Which statement is the safest engineering habit when using Write-Ahead Log (WAL)?
Pick one answer.
You have dashboards for traffic, latency, errors and saturation. You can change the architecture, but every change has operational cost.
Production scenario: your system uses Write-Ahead Log (WAL), traffic suddenly spikes, and p99 latency doubles. What is your first move?
Interview drill
Answer this without notes: When would you choose Write-Ahead Log (WAL), and when would you intentionally avoid it? Mention at least one bottleneck it addresses, one failure mode it introduces, and one alternative. Then quantify the workload you are designing for.
For Write-Ahead Log (WAL), start with access patterns rather than brand names. Identify the dominant reads/writes, data relationships, consistency requirements, partition key, hot keys and failure behavior before choosing a storage strategy.
Numerical sanity check
A rough capacity check: required write throughput ≈ peak writes/s × average record size. At 5,000 writes/s and 2 KB average payloads, the raw incoming data stream is about 10 MB/s before indexes, replication and overhead.
Do not optimize for a memorized definition. Reason from the workload and failure mode.
A team proposes Write-Ahead Log (WAL) because it 'scales'. What workload characteristic would make that choice a poor fit?
Pick one answer.
What you gain, what you pay
- +Enables ACID durability — committed transactions survive crashes.
- +Sequential appends are fast and crash-safe; data files can be updated lazily.
- +Foundation for streaming replication, PITR, and CDC.
- +Foundation for the transactional outbox pattern (atomic DB + event publishing).
- +Recovery is automatic and correct on restart.
- −Every commit pays an fsync latency cost (mitigated by group commit, NVMe, or async commit).
- −WAL storage grows; must be archived or recycled after checkpoints.
- −Synchronous replication adds round-trip latency to commits.
- −Crash recovery time is bounded by the distance since the last checkpoint.
- −WAL throughput can become the write bottleneck on high-write workloads.
How this breaks in production
- fsync disabled or ignored by storage layer (e.g., lying SSDs) — silent data loss on power failure.
- WAL disk full — database refuses new commits until space is freed.
- Replication slot lag — replicas fall behind, WAL accumulates on primary.
- Checkpoint spikes — aggressive checkpointing saturates I/O periodically.
- Long recovery time — checkpoints too infrequent means long replay on crash.
- Corrupted WAL — rare but catastrophic; the database cannot start.
Don't fall into these traps
- •Setting `fsync = off` for performance without understanding the durability loss.
- •Disabling `synchronous_commit` on critical (e.g., payment) workloads.
- •Not sizing WAL storage — a disk-full WAL halts all writes.
- •Not archiving WAL — losing the ability to do point-in-time recovery.
- •Leaving replication slots open on replicas that no longer consume — primary WAL grows indefinitely.
- •Forgetting that lying storage (consumer-grade SSDs that lie about fsync) breaks durability silently.
Real systems using this
How real systems implement this
- PostgreSQL — pg_wal/ directory holds WAL segment files (default 16MB each). Streaming replication sends WAL records over a socket; physical replicas replay them. Logical decoding (wal_level=logical) powers CDC tools like Debezium and AWS DMS.
- MySQL / InnoDB — Has TWO logs: the InnoDB redo log (crash recovery) and the MySQL binlog (server-level replication). They're coordinated by an internal XA. The binlog is the foundation of statement-based and row-based replication in MySQL ecosystems.
- Oracle — Redo log files in groups (online redo logs) cycle through; archive logs are saved copies used for PITR. Oracle's Data Guard uses redo log shipping for standby databases — same pattern as PostgreSQL streaming replication.
- Debezium (CDC across many databases) — Reads the WAL (PostgreSQL), binlog (MySQL), redo log (Oracle), or transactional replication (SQL Server) to stream row-level changes to Kafka. No load on the application, no dual-write problem — the WAL is the source of truth.
Practice saying it out loud
- Q1What is a write-ahead log, and why does every transactional database have one?
- Q2Walk through what happens when a PostgreSQL database crashes mid-write. How does it recover?
- Q3What does `synchronous_commit = off` do, and when is it acceptable?
- Q4How does the WAL enable the transactional outbox pattern?
- Q5How does streaming replication use the WAL? What does 'replication lag' actually mean?
- Q6What's the relationship between checkpoints, WAL size, and crash recovery time?
Further reading & references
Core explanations are original NO CAP material. External references are provided for deeper study and standards.
What next?
Mark as understood once the mental model clicks.
Next recommended
Event Sourcing