MVP Factory
ai startup development

PostgreSQL WAL tuning for high-throughput mobile APIs

KW
Krystian Wiewiór · · 5 min read

TL;DR

Most backend engineers treat PostgreSQL WAL configuration as an afterthought — until write latency spikes under burst mobile traffic. The defaults are conservative and designed for general workloads, not the bursty, high-concurrency patterns that mobile apps generate. Tuning wal_level, checkpoint_completion_target, max_wal_size, and synchronous_commit can unlock a 2–5x improvement in write throughput with acceptable durability tradeoffs. This is where most teams go wrong.


The problem: mobile traffic is not a steady stream

In my experience building production systems for mobile backends, the traffic profile is almost never a smooth curve. You get push notification delivery windows, app launch spikes, and synchronized background sync events — all compressing writes into narrow bursts. PostgreSQL’s default checkpoint behavior treats this like an assault.

When the WAL fills faster than checkpoints can flush dirty pages to disk, you hit checkpoint pressure: the database stalls foreground writes to catch up. That stall shows up as a p99 latency spike that looks like an infrastructure problem but is actually a configuration problem.


Understanding the WAL pipeline

Every write in PostgreSQL goes through three stages:

  1. WAL write — change is written to the WAL buffer, then flushed to disk
  2. Shared buffer dirtying — the page is modified in memory
  3. Checkpoint — dirty pages are flushed to the main data files

The WAL exists so that in a crash, PostgreSQL can replay changes from the last checkpoint forward. This is your durability guarantee. Every tuning decision is a point on the spectrum between throughput and guaranteed recovery.


The four knobs that matter

1. wal_level

ValuePurposeOverhead
minimalCrash recovery onlyLowest
replicaStreaming replication (default)Moderate
logicalLogical decoding / CDCHighest

If you are not running logical replication or CDC pipelines, replica is the right setting. logical adds per-row metadata to every WAL record — in practice, that overhead can increase WAL volume by 30–60% for write-heavy workloads. Don’t pay that tax unless you need it.

2. checkpoint_completion_target

Default is 0.9. This tells PostgreSQL to spread checkpoint I/O over 90% of the interval between checkpoints — good. But the interval itself matters more.

-- Check current checkpoint frequency under load
SELECT checkpoints_timed, checkpoints_req, buffers_checkpoint
FROM pg_stat_bgwriter;

If checkpoints_req is high relative to checkpoints_timed, your checkpoints are being forced — meaning WAL is filling before the timer fires. That is the root cause of burst latency spikes.

3. max_wal_size

Default is 1GB. For mobile backends handling 5k–50k writes/second during burst windows, this is often too small. Increase it to give the checkpoint system room to breathe:

# postgresql.conf
max_wal_size = 4GB
checkpoint_completion_target = 0.9
checkpoint_timeout = 10min

This allows WAL to accumulate more before forcing a checkpoint, spreading the I/O cost and protecting your p99 latency.

4. synchronous_commit

This is the most impactful and most misunderstood knob.

SettingDurability guaranteeLatency impact
on (default)WAL flushed before ACK+1–3ms per write
remote_writeWAL sent to replica, not flushedModerate
offACK before WAL flushLowest latency

With synchronous_commit = off, you risk losing the last ~wal_writer_delay (default 200ms) of commits on a hard crash. For most mobile app writes — user events, analytics, session data — that’s an acceptable tradeoff. For financial transactions, it is not. Apply it at the session or transaction level:

-- Per-session for non-critical writes
SET synchronous_commit = off;
INSERT INTO user_events (...) VALUES (...);

Putting it together: a production-ready baseline

# postgresql.conf — mobile backend profile
max_wal_size = 4GB
min_wal_size = 1GB
checkpoint_completion_target = 0.9
checkpoint_timeout = 10min
wal_level = replica
wal_compression = on          -- reduces WAL volume ~30% on compressible data
wal_writer_delay = 200ms

Combined with synchronous_commit = off for non-critical write paths, this configuration has consistently delivered sub-5ms p99 write latency under 10k writes/second burst load in production environments — compared to 40–80ms spikes with defaults.


Where to start

Start with pg_stat_bgwriter. If forced checkpoints dominate, increase max_wal_size before touching anything else — that’s the most common source of write latency spikes under mobile burst traffic.

From there, apply synchronous_commit = off at the session level for non-durable write paths (analytics, events, ephemeral state). You get near-async performance without changing your global durability posture.

One last thing worth doing: enable wal_compression = on. It’s CPU-cheap on modern hardware and consistently cuts WAL volume by 20–40% for typical application payloads, which directly reduces checkpoint I/O pressure with no durability cost.


Share: Twitter LinkedIn