---
title: "PostgreSQL WAL Tuning for High-Throughput Mobile APIs"
published: true
description: "Tune PostgreSQL WAL for mobile backends — checkpoint pressure, wal_level, synchronous_commit tradeoffs, and the configs that eliminate write latency spikes under burst traffic."
tags: postgresql, api, mobile, architecture
canonical_url: https://mvpfactory.co/blog/postgresql-wal-tuning-mobile-backends
---
What We Are Building
By the end of this tutorial you will have a production-ready PostgreSQL WAL configuration for mobile backends. We will walk through the four knobs that actually matter — wal_level, checkpoint_completion_target, max_wal_size, and synchronous_commit — and you will understand exactly why each one exists and when to reach for it.
The target outcome: sub-5ms p99 write latency under 10k writes/second burst load, compared to the 40–80ms spikes you get with defaults.
Prerequisites
- A running PostgreSQL instance (14+)
- Access to
postgresql.conf - Basic familiarity with
psql - A write-heavy workload to tune against (or a willingness to simulate one)
The Problem: Mobile Traffic Is Not a Steady Stream
Let me show you a pattern I see in every production mobile backend. Traffic is never a smooth curve. Push notification delivery windows, app launch spikes, 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. It is actually a configuration problem.
Step 1: Understand the WAL Pipeline
Every write in PostgreSQL moves through three stages:
- WAL write — change is written to the WAL buffer, then flushed to disk
- Shared buffer dirtying — the page is modified in memory
- Checkpoint — dirty pages are flushed to the main data files
The WAL is your crash recovery guarantee. Every tuning decision you make is a point on the spectrum between throughput and guaranteed recovery. Keep that in mind as we work through each knob.
Step 2: Diagnose Before You Tune
Here is the minimal setup to get this working — start with observation, not configuration changes.
-- 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 — the WAL is filling before the timer fires. That is the root cause of burst latency spikes. Fix that first.
Step 3: Set wal_level Correctly
| Value | Purpose | Overhead |
|---|---|---|
minimal |
Crash recovery only | Lowest |
replica |
Streaming replication (default) | Moderate |
logical |
Logical decoding / CDC | Highest |
If you are not running logical replication or CDC pipelines, replica is the right setting. The docs do not make this obvious, but logical adds per-row metadata to every WAL record — in practice that increases WAL volume by 30–60% for write-heavy workloads. Do not pay that tax unless you need it.
Step 4: Give Checkpoints Room to Breathe
The default max_wal_size is 1GB. For mobile backends handling 5k–50k writes/second during burst windows, this is often too small. Increase it so the checkpoint system has room to operate without forcing stalls:
# postgresql.conf
max_wal_size = 4GB
checkpoint_completion_target = 0.9
checkpoint_timeout = 10min
checkpoint_completion_target = 0.9 spreads checkpoint I/O over 90% of the interval between checkpoints — already the default, but pair it with a longer interval and larger WAL size and you eliminate most forced checkpoints under burst load.
Step 5: Apply synchronous_commit Selectively
This is the most impactful and most misunderstood knob. Here is the gotcha that will save you hours: you do not have to set this globally.
| Setting | Durability guarantee | Latency impact |
|---|---|---|
on (default) |
WAL flushed before ACK | +1–3ms per write |
remote_write |
WAL sent to replica, not flushed | Moderate |
off |
ACK before WAL flush | Lowest 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 is an acceptable tradeoff. For financial transactions, it is not.
Apply it at the session level for non-critical write paths:
-- Per-session for non-critical writes
SET synchronous_commit = off;
INSERT INTO user_events (...) VALUES (...);
This gives you near-async performance without changing your global durability posture.
Step 6: The 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
Enable wal_compression = on. It is 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. The docs do not emphasize this enough.
Gotchas
Increasing max_wal_size means longer crash recovery. More WAL to replay. On most hardware with modern SSDs this is a non-issue, but factor it into your RTO if you are in a regulated environment.
synchronous_commit = off is not the same as async transactions. The transaction still commits — you just may lose the very last window of writes on a hard crash. Understand what data lives in that window before you flip this.
Forced checkpoints are the real enemy. Teams often reach for synchronous_commit first because the latency gain is obvious. Check pg_stat_bgwriter first. If forced checkpoints are high, increasing max_wal_size will do more good with zero durability tradeoff.
wal_level = logical creep. If someone enabled logical replication for a CDC experiment that was later decommissioned, this setting often gets left behind. Audit it.
Conclusion
PostgreSQL's WAL defaults are conservative and designed for general workloads — not the bursty, high-concurrency patterns that mobile apps generate. With max_wal_size = 4GB, checkpoint_timeout = 10min, wal_compression = on, and synchronous_commit = off applied selectively to non-critical write paths, this configuration has consistently delivered sub-5ms p99 write latency under 10k writes/second burst load in production — compared to 40–80ms spikes with defaults.
Start with pg_stat_bgwriter. If forced checkpoints dominate, increase max_wal_size before touching anything else. That single change fixes the majority of write latency problems I see in mobile backend production systems.
Further reading:
Top comments (0)