Postgres and MongoDB Diverge on Write Amplification Costs
Every database write costs more than one write. Postgres and MongoDB diverge on where that extra cost lands, and the bill arrives as SSD endurance, latency spikes, or cloud storage charges. This piece traces each engine's write path and explains which workloads each one handles better.
The Write Path Nobody Budgets For
Both Postgres and MongoDB rewrite data on every update. A single logical write becomes multiple physical writes: the new version, the journal entry, the index update, and eventually the old version's cleanup. That multiplication is write amplification, and it's the hidden line item in database operations.
SSDs pay for it in endurance. Flash cells tolerate a finite number of program-erase cycles, so extra writes shorten drive life. Cloud providers bill for it as provisioned IOPS or storage throughput, which makes amplification a line item rather than an abstraction.
The architectural choices diverge early. Postgres keeps a heap of row versions and relies on vacuum to reclaim them. MongoDB's WiredTiger storage engine journals writes and checkpoints them into compressed files. Both approaches have consequences for latency and storage.
Write amplification is not a fixed multiplier. It depends on workload, schema, and configuration. A write-heavy OLTP system might see amplification factors in the 2–10x range, while a read-mostly system with occasional updates might stay near 1.5x. The variance is what makes it hard to budget for.
Postgres Rewrites Tuples In Place
Postgres uses multi-version concurrency control, or MVCC. An update doesn't overwrite the old row; it writes a new tuple version and marks the old one as dead. Readers see a consistent snapshot without blocking writers, which is a genuine strength for transactional workloads.
Dead tuples linger until vacuum runs. Autovacuum triggers based on thresholds, and under heavy update load it can fall behind. Table bloat grows, indexes point to dead entries, and sequential scans read more pages than necessary. The storage cost roughly doubles in bad cases.
Every index on a changed column gets a new entry. A table with five indexes turns one logical update into six physical writes plus the heap tuple. That's the amplification Postgres operators live with, and it's why index-heavy schemas hurt more.
Consider a concrete example: a table with 10 million rows, each with a primary key and two secondary indexes. An update to a single non-indexed column still writes a new heap tuple (8 KB page) and updates the primary key index (since the tuple's physical location changes). That's two writes for one logical update. If the update touches an indexed column, it's three writes. Multiply by 10,000 updates per second and you're writing 30,000 times per second to storage, even though the application sees 10,000 writes.
The write amplification factor (WAF) for Postgres often lands between 2 and 5 for typical OLTP workloads, but can exceed 10 for update-heavy tables with many indexes. That factor directly impacts SSD wear and cloud IOPS costs.
There's a trade-off between autovacuum aggressiveness and I/O load. Running vacuum more frequently keeps bloat in check but adds read I/O and CPU overhead. Setting autovacuum_vacuum_cost_delay to 0 makes vacuum run at full speed, which works on fast storage but can starve foreground queries on shared instances. The right setting depends on your storage latency and write concurrency. For high-update tables, lowering autovacuum_vacuum_scale_factor from its default 0.2 to something like 0.05 can help, at the cost of more frequent vacuum runs.
MongoDB Pays At The Document Level
WiredTiger journals every write before acknowledging it, then checkpoints the journal into the main data files periodically. The journal is append-only, which is efficient, but the checkpoint rewrites pages that changed since the last one. That's the core amplification loop.
Document growth triggers relocation. If an update makes a document larger than its allocated space, WiredTiger moves it and updates the index. Frequent growth on large documents causes fragmentation and extra I/O. Padding can help, but it wastes space on documents that never grow.
Indexes are updated for every changed field. A document with ten indexed fields touches ten index entries per write. Compression reduces the bytes written but not the number of write operations, so endurance costs remain.
WiredTiger's checkpoint interval defaults to 60 seconds, meaning the journal accumulates writes between checkpoints. At checkpoint, all dirty pages are flushed, which can cause a burst of I/O. If the checkpoint takes longer than the interval, writes stall. This is why checkpoint tuning matters for write-heavy workloads.
The journal itself is written with a configurable compression (snappy by default), which reduces the bytes written but not the number of I/O operations. The WAF for MongoDB typically ranges from 2 to 6, depending on document size and update patterns.
WiredTiger's cache size also affects amplification. When the cache fills, eviction writes pages to disk more aggressively, increasing write volume. The default cache size is half of available RAM minus 1 GB, but for write-heavy workloads, increasing it can reduce eviction pressure. However, larger caches mean longer checkpoint times, which can cause stalls if the checkpoint interval is too short. Tuning these parameters requires understanding your working set and write rate.
Where Each Engine Genuinely Wins
Postgres handles transactional updates well. Short transactions that touch a few rows produce predictable amplification, and vacuum keeps up when configured properly. The engine's strength is consistency under concurrent writes, not raw write throughput.
MongoDB suits append-heavy workloads. Inserting new documents avoids relocation and keeps journal writes sequential. Sharded clusters spread the write load across nodes, which reduces per-node amplification. This site has argued in a related piece on partition ownership in Kafka and NATS that distribution shifts costs rather than removing them, and the same logic applies here.
Both engines suffer under random small writes. A workload that updates one field across millions of documents forces index churn and page rewrites in either system. The difference is where the pain surfaces: vacuum lag in Postgres, checkpoint stalls in MongoDB.
Workload shape decides the winner. Read-heavy with occasional updates favors Postgres. Write-heavy with document inserts favors MongoDB. Mixed workloads with frequent updates penalize both, and the right answer may be a different storage model entirely.
What Operators Actually See
Vacuum lag is the leading indicator in Postgres. When autovacuum can't keep pace, table bloat grows, query plans shift to sequential scans, and latency climbs. Monitoring dead tuple counts and vacuum progress catches the problem before it becomes an outage.
MongoDB shows checkpoint stalls as latency spikes. When the checkpoint takes longer than the interval, writes queue up and application response times jump. Journal lag and cache eviction rates are the metrics to watch. The registry quota debate on this site touches a similar theme: operational limits surface as user-visible failures.
Few teams monitor write amplification directly. It's easier to watch storage growth and cloud bills, which reveal the gap after the fact. A Postgres instance with heavy updates might write several times the logical data volume; a MongoDB replica set with large documents can do the same.
Cloud storage bills are the blunt instrument. When provisioned IOPS or throughput costs climb faster than data volume, amplification is the likely cause. That's the signal to investigate rather than assume.
For Postgres, tools like pg_stat_statements and pg_stat_user_tables can reveal update patterns and dead tuple counts. For MongoDB, serverStatus and collStats provide write metrics. Neither gives a direct WAF number, but you can infer it from disk write bytes vs. application write volume.
Measure Before You Migrate
Benchmark your real write mix. Synthetic benchmarks rarely match production patterns. Capture a representative sample of updates, inserts, and deletes, then replay it against both engines. Measure bytes written per logical write at the storage layer, not just query latency.
Tune vacuum or checkpoint intervals based on that data. Postgres autovacuum settings need adjustment for high-update tables; MongoDB checkpoint intervals and journal compression settings affect write volume. Defaults favor general workloads, not yours.
Consider append-only or LSM-tree alternatives when amplification is the bottleneck. Databases built on log-structured merge trees, such as Cassandra or ScyllaDB, trade read amplification for lower write costs. This site's piece on edge networking careers makes a parallel point about trade-offs that don't disappear.
Re-evaluate after schema changes. Adding an index or changing a document structure alters the write path. A configuration that worked six months ago may not fit today's workload. Measurement is a recurring task, not a one-time migration step.