Files
landing_page/qdrant-landing/content/documentation/data-synchronization/change-data-capture.md
T

6.9 KiB

title, weight
title weight
Tier 3: Change Data Capture 40

Tier 3: Change Data Capture with Debezium + Redpanda

"Let Postgres tell Qdrant what changed"

Architecture

CDC is architecturally different from dual-writes and the transactional outbox in a fundamental way: the application code has no awareness of Qdrant. The FastAPI routes are pure Postgres CRUD — they don't import the Qdrant client, they don't write to an outbox. Sync is handled entirely in the infrastructure layer.

How It Works

Postgres's Write-Ahead Log (WAL) is a sequential log of every change to the database — it exists for crash recovery and replication. With wal_level = logical, external consumers can read this log in a structured format.

Debezium connects to Postgres via a logical replication slot and captures every INSERT, UPDATE, and DELETE on the products table as a JSON event. These events are published to a Redpanda topic. A Python consumer service reads from that topic and calls Qdrant.

Postgres Setup

-- Enable in postgresql.conf (or docker-compose command args):
-- wal_level = logical

-- Create a publication for the products table
CREATE PUBLICATION products_publication FOR TABLE products;

Debezium Connector Config

{
    "name": "products-connector",
    "config": {
        "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
        "database.hostname": "postgres",
        "database.port": "5432",
        "database.user": "debezium",
        "database.dbname": "fashiondb",
        "database.server.name": "pgserver",
        "table.include.list": "public.products",
        "plugin.name": "pgoutput",
        "publication.name": "products_publication",
        "slot.name": "qdrant_sync",
        "topic.prefix": "pgserver",
        "transforms": "unwrap",
        "transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState",
        "transforms.unwrap.delete.handling.mode": "rewrite"
    }
}

The ExtractNewRecordState transform unwraps Debezium's envelope format into a flat payload, and adds an __op field (c = create, u = update, d = delete) so the consumer doesn't have to parse the raw Debezium schema.

The Consumer Service

from confluent_kafka import Consumer

def consume_and_sync():
    consumer = Consumer({
        "bootstrap.servers": "redpanda:9092",
        "group.id": "qdrant-sync",
        "auto.offset.reset": "earliest",
        "enable.auto.commit": "false",  # manual commit after successful processing
    })
    consumer.subscribe(["pgserver.public.products"])

    while True:
        msg = consumer.poll(1.0)
        if msg is None:
            continue

        event = json.loads(msg.value())
        op = event.get("__op")  # 'c', 'u', or 'd'

        if op in ("c", "u"):
            upsert_to_qdrant(event)
        elif op == "d":
            delete_from_qdrant(event["article_id"])

        consumer.commit()  # commit only after successful processing

Manual offset commits (committing only after successfully processing each message) give you at-least-once semantics: if the consumer crashes mid-message, it reprocesses from the last committed offset on restart.

Redpanda vs. Kafka

The example uses Redpanda as the event bus. Redpanda is Kafka-wire-protocol-compatible but ships as a single binary with no ZooKeeper dependency — simpler to run. The consumer code is identical whether you point it at Redpanda or Apache Kafka; just change bootstrap.servers.

The Killer Feature: Replaying History

CDC's most underappreciated advantage is replayability. If Qdrant needs to be rebuilt from scratch — new index configuration, migration to a new cluster, disaster recovery — you can do it by replaying the Redpanda topic from the beginning. Every change that ever happened to the products table is recorded in the event stream.

With dual-write or the outbox, a full rebuild requires running a bulk export from Postgres. With CDC, the event stream is the rebuild mechanism.

Failure Modes

Failure Consequence Mitigation
Qdrant is down Consumer pauses; Redpanda retains events Consumer retries; resumes from offset on recovery
Redpanda is down Debezium buffers; WAL grows Monitor WAL size; Redpanda HA with replication in production
Debezium crashes WAL retains changes since last checkpoint Debezium resumes from replication slot on restart
Schema change in Postgres Connector may need restart Monitor connector status; test schema migrations in staging

The WAL Disk Bloat Problem

One operational hazard specific to CDC: Postgres holds WAL segments until the replication slot consumer acknowledges them. If your consumer is down for a long time, Postgres can accumulate significant disk usage. Monitor replication slot lag:

SELECT
    slot_name,
    pg_size_pretty(
        pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)
    ) AS lag
FROM pg_replication_slots;

Set up alerting if this exceeds a few GB. In extreme cases (consumer permanently down), you may need to drop the replication slot — which means re-seeding Qdrant from Postgres rather than from the event stream.

When to Use This

  • High write throughput systems where the outbox table would become a bottleneck
  • When multiple downstream consumers need to react to changes (not just Qdrant)
  • When application code must be fully decoupled from sync concerns
  • Teams with existing Redpanda/Kafka infrastructure
  • When replay-from-scratch capability is a requirement

Tier 3 is genuinely powerful, but it comes with real operational costs: Redpanda and Debezium to deploy, configure, monitor, and upgrade. If you're not already running streaming infrastructure, think hard before adding it for a single sync use case. Tier 2 handles most production scenarios with far less complexity.

Comparison Matrix

Dimension Tier 1: Dual-Write Tier 2: Outbox Tier 3: CDC
Sync complexity Low Medium High
Extra infrastructure None Outbox table + worker Redpanda + Debezium + consumer
Consistency model Best-effort At-least-once, eventual At-least-once, eventual
Write latency impact Adds Qdrant round-trip None (async) None (async)
Qdrant downtime impact Generates drift Events queue in Postgres Events queue in Redpanda
Captures direct SQL No No Yes (all WAL changes)
Replay capability No Limited (outbox retention) Yes (Redpanda retention)
Operational overhead Minimal Low-Medium High
Best for Prototypes, internal tools Most production apps High-throughput, multi-consumer