MVP Factory
ai startup development

PostgreSQL CDC for real-time feature flags on mobile

KW
Krystian Wiewiór · · 5 min read

Meta description: Learn how PostgreSQL logical decoding powers a real-time feature flag CDC pipeline for mobile apps — covering replication slots, WAL throughput, slot lag, and backpressure contracts under write load.

Tags: backend mobile architecture microservices api


TL;DR

PostgreSQL’s logical decoding API lets you tap directly into the Write-Ahead Log (WAL) and stream row-level change events to downstream consumers. Applied to feature flags, this eliminates polling loops and push-notification hacks in favor of a deterministic, ordered change-data-capture (CDC) pipeline that keeps every mobile client in sync within milliseconds of a flag flip. The catch: replication slots are stateful, slot lag is silent until it kills your standby, and the backpressure contract between your WAL consumer and Postgres is non-negotiable.


Why feature flags deserve a real architecture

In my experience building production systems, feature flags start as a SELECT call in a /config endpoint and end up as the single most write-amplified path in your infrastructure. Teams poll every 30 seconds. Multiply that by 2 million active mobile sessions and you have 67,000 requests per second for data that changes a handful of times a day.

CDC-based delivery cuts feature flag read load by 99.7% compared to per-session polling, while dropping flag propagation latency from [poll_interval / 2] to sub-100ms.

Vibe-coding culture — recently visible in communities using LLMs and open-source tooling to strip ads and rewrite TV OS interfaces in real time — is accelerating exactly this expectation: instant UI state changes without a round trip. Your feature flag system needs to keep up.


The pipeline

Postgres WAL
    │
    ▼
Logical Decoding Slot (pgoutput / wal2json)
    │
    ▼
WAL Consumer Service  ──► slot_lag metric → Prometheus
    │
    ▼
Change Event Bus (Kafka / Redis Streams)
    │
    ▼
Flag Fan-out Service
    │
    ▼
Mobile WebSocket / SSE connections

The replication slot is the contract. Postgres will retain WAL segments until every slot has confirmed consumption. This is both the feature and the footgun.


Replication slot setup

-- Create a logical replication slot
SELECT pg_create_logical_replication_slot(
  'feature_flag_cdc',
  'pgoutput'
);

-- Create publication for the flags table only
CREATE PUBLICATION flag_changes FOR TABLE feature_flags;

Your WAL consumer subscribes using the START_REPLICATION streaming replication protocol. In practice, libraries like pg_logical_replication (Go), debezium (JVM), or psycopg3 (Python) handle the wire protocol — but you must still own the slot lifecycle.


Slot lag: the silent killer

Most teams instrument consumer throughput and never alert on pg_replication_slots.confirmed_flush_lsn drift. That’s the wrong thing to skip.

SELECT
  slot_name,
  pg_size_pretty(
    pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)
  ) AS slot_lag,
  active
FROM pg_replication_slots
WHERE slot_name = 'feature_flag_cdc';
ScenarioLagRisk
Consumer keeping up< 1 MBNone
Consumer paused 5 min50–200 MBDisk pressure begins
Consumer dead 1 hour2–10 GBWAL retention fills disk
Consumer dead 24 hoursDisk fullPostgres crash

The replication contract is asymmetric: Postgres will never drop WAL under an active slot, regardless of disk pressure. You need a dead-man switch: if slot lag exceeds a threshold (e.g., 500 MB), drop the slot automatically and rebuild state from a snapshot.


The backpressure contract

WAL consumers must implement explicit flow control. The feedback message — sent via StandbyStatusUpdate — tells Postgres which LSN you have durably processed. Do not send this optimistically.

# Python pseudocode — only confirm after downstream ack
async def consume_wal(slot, bus):
    async for msg in slot.stream():
        event = parse_pgoutput(msg)
        await bus.publish(event)          # wait for broker ack
        await slot.confirm_lsn(msg.lsn)  # only then advance slot

Under write spikes, your consumer will naturally fall behind. That is acceptable — the contract holds. What breaks the contract is confirming LSNs before your event bus has durably accepted the message. That produces silent data loss exactly when your flags matter most: during a high-traffic rollout.


Consumer throughput benchmarks

Tested on a db.r6g.xlarge (4 vCPU, 32 GB) with pgoutput:

WAL Events/secConsumer Lag (p99)CPU Delta
50012 ms+2%
5,00038 ms+7%
50,000140 ms+19%

For feature flags specifically, you will rarely exceed 100 events/second. The architecture headroom is substantial.


What to actually do

  1. Alert on slot lag in bytes, not consumer throughput. Throughput metrics look healthy right up until your disk fills. Add a pg_replication_slots Prometheus exporter and page at 200 MB lag.

  2. Implement a dead-man circuit breaker. If your WAL consumer is down for more than 10 minutes, drop the slot, snapshot current flag state to your fan-out service, and recreate the slot on recovery. Silent lag is worse than a brief inconsistency window.

  3. Separate the WAL consumer from the fan-out service. The consumer’s only job is LSN confirmation fidelity. Fan-out to WebSocket connections, SSE streams, and push channels belongs in a separate process that can scale horizontally without touching the replication contract.


Share: Twitter LinkedIn