DEV Community

SoftwareDevs mvpfactory.io
SoftwareDevs mvpfactory.io

Posted on Originally published at mvpfactory.io

PostgreSQL Write-Ahead Log Tuning for Mobile Backends

---
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
---
Enter fullscreen mode Exit fullscreen mode

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:

  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 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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 (...);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)