---
title: "PostgreSQL Logical Decoding for Real-Time Feature Flags: CDC Pipelines, Slot Lag, and the Replication Contract"
published: true
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: postgresql, mobile, architecture, api
canonical_url: https://mvpfactory.co/blog/postgresql-cdc-feature-flags-mobile
---
What We Are Building
By the end of this workshop, you will have a working mental model — and the SQL and code to back it up — for a change-data-capture pipeline that streams PostgreSQL row-level flag changes to mobile clients in milliseconds. No polling loops. No push-notification hacks. Just a deterministic, ordered event stream from your WAL to your WebSocket connections.
Here is the pattern I use in every project where feature flag propagation latency actually matters.
Prerequisites
- PostgreSQL 10+ (logical replication enabled)
- A
feature_flagstable you own - A WAL consumer library:
debezium(JVM),pg_logical_replication(Go), orpsycopg3(Python) - Prometheus or equivalent for slot lag alerting — this is non-negotiable
Why Feature Flags Need a Real Architecture
Feature flags start as a SELECT call in a /config endpoint. They 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 propagation latency from [poll_interval / 2] to sub-100ms. The architecture is worth understanding.
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 retains WAL segments until every slot has confirmed consumption. That is both the feature and the footgun.
Step 1 — Create the Replication Slot and Publication
Here is the minimal setup to get this working:
-- Create a logical replication slot
SELECT pg_create_logical_replication_slot(
'feature_flag_cdc',
'pgoutput'
);
-- Scope the publication to flags only — do not publish everything
CREATE PUBLICATION flag_changes FOR TABLE feature_flags;
Your WAL consumer subscribes using the START_REPLICATION streaming replication protocol. The libraries handle the wire protocol, but you must own the slot lifecycle. That distinction matters when things go wrong at 3am.
Step 2 — Implement the Backpressure Contract
The docs do not explain this clearly enough: the StandbyStatusUpdate feedback message tells Postgres which LSN you have durably processed. Do not send it optimistically.
# Only confirm after your downstream broker has acknowledged
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 the slot
Under write spikes your consumer will fall behind. That is acceptable — the contract holds. What breaks it 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.
Step 3 — Monitor Slot Lag in Bytes
Here is the gotcha that will save you hours: most teams instrument consumer throughput and never alert on confirmed_flush_lsn drift. Throughput metrics look healthy right up until your disk fills.
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';
This is what the risk curve looks like in practice:
| Scenario | Lag | Risk |
|---|---|---|
| Consumer keeping up | < 1 MB | None |
| Consumer paused 5 min | 50–200 MB | Disk pressure begins |
| Consumer dead 1 hour | 2–10 GB | WAL retention fills disk |
| Consumer dead 24 hours | Disk full | Postgres crash |
Postgres will never drop WAL under an active slot, regardless of disk pressure. Alert at 200 MB lag. Page at 500 MB.
Step 4 — Build a Dead-Man Circuit Breaker
If slot lag exceeds 500 MB, drop the slot automatically, snapshot current flag state to your fan-out service, and recreate the slot on recovery. A brief inconsistency window is far better than silent lag growing toward a crash.
Tested benchmarks on a db.r6g.xlarge (4 vCPU, 32 GB) with pgoutput:
| WAL Events/sec | Consumer Lag (p99) | CPU Delta |
|---|---|---|
| 500 | 12 ms | +2% |
| 5,000 | 38 ms | +7% |
| 50,000 | 140 ms | +19% |
For feature flags you will rarely exceed 100 events/second. The architecture headroom is substantial.
Gotchas
Slot lag is a disk problem, not a throughput problem. Do not monitor consumer events/sec and call it done. Add a pg_replication_slots Prometheus exporter and alert on bytes, not rates.
Never share your WAL consumer process with your 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.
pgoutput requires a publication. Unlike wal2json, pgoutput is the built-in logical decoder and requires a named PUBLICATION. Scoping it to a single table (FOR TABLE feature_flags) is not optional — it is how you keep WAL volume manageable.
Dropped slots lose their position. When you rebuild from a snapshot after a dead-man trigger, your fan-out service must treat the snapshot as authoritative ground truth. Do not try to reconcile partial WAL history.
Conclusion
PostgreSQL logical decoding gives you a production-grade CDC primitive without additional infrastructure. For feature flags specifically, the architecture is almost embarrassingly well-suited: low event volume, high fan-out value, and latency requirements that polling simply cannot meet at scale.
The replication contract is the piece most teams skip until it hurts. Own the slot lifecycle, alert on lag in bytes, and separate your WAL consumer from your fan-out service. Everything else is implementation detail.
Further reading:
Top comments (0)