Skip to main content
Version: 1.9.0

PostgreSQL CDC

The PostgreSQL CDC publisher captures row-level changes (inserts, updates, deletes) from a PostgreSQL database using WAL logical replication. Changes are decoded by the wal2json output plugin (format version 2), buffered in memory, and staged in controlled batches.

Note: PostgreSQL CDC is currently marked as unstable and may undergo API changes in future releases.

Example

from typing import Tuple
import tabsdata as td

conn = td.PostgresCdcConn(
uri="postgresql://localhost:5432/ecommerce",
credentials=td.UserPasswordCredentials(
user=td.EnvironmentSecret("PG_USER"),
password=td.EnvironmentSecret("PG_PASS"),
),
)

trigger = td.PostgresCdcTrigger(
conn=conn,
tables=["public.orders", "public.order_items"],
start_from="tail",
replication_slot="ecommerce_cdc_slot",
slot_behavior="create",
)

@td.publisher(
trigger=trigger,
tables=["orders", "order_items"],
)
def capture_ecommerce(
orders: list[td.TableFrame],
order_items: list[td.TableFrame],
) -> Tuple[td.TableFrame, td.TableFrame]:
return td.concat(orders), td.concat(order_items)

This example publishes CDC data for the orders and order_items tables, capturing only changes that occur after the publisher has been first registered.

After defining the function, register it with a Tabsdata collection and trigger its execution.

Setup

PostgreSQL must be configured to enable CDC before using this publisher. See PostgreSQL Setup to Enable CDC.

Connection: PostgresCdcConn

PostgresCdcConn defines how to connect to the PostgreSQL server. It accepts a standard PostgreSQL URI and optional credentials.

conn = td.PostgresCdcConn(
uri="postgresql://localhost:5432/my_database",
credentials=td.UserPasswordCredentials(
user=td.EnvironmentSecret("PG_CDC_USER"),
password=td.EnvironmentSecret("PG_CDC_PASSWORD"),
),
)
ParameterTypeDescription
uristrPostgreSQL connection URI ( postgresql://host:port/database ). If the port is omitted, defaults to 5432 . If the database is omitted, defaults to "postgres" .
credentialsUserPasswordCredentialsNone
cx_src_configs_postgresdictNone

Trigger: PostgresCdcTrigger

PostgresCdcTrigger connects to PostgreSQL, reads WAL events for the specified tables via logical replication, and stages batches of changes.

trigger = td.PostgresCdcTrigger(
conn=conn,
tables=["public.orders", "public.order_items"],
start_from="tail",
replication_slot="ecommerce_cdc_slot",
slot_behavior="create",
)

tables

Specifies which database tables to monitor. Tables must be fully qualified as schema.table. Accepts a single string or a list of strings.

# Single table
tables="public.orders"

# Multiple tables
tables=["public.orders", "public.order_items"]

All tables must exist in the source database before the trigger starts. Tables created after the trigger is running will not be captured.

Note: Schema changes (such as ALTER TABLE, ADD COLUMN, DROP COLUMN) on tracked tables are handled automatically. No additional configuration is required — the connector detects the change and adjusts its output accordingly.

start_from

Determines where the connector begins reading from the WAL. On subsequent runs, the connector resumes automatically from its last committed offset.

ValueTypeBehavior
"head"strStart from the earliest available position in the WAL.
"tail"strStart from the current end of the WAL, capturing only new events.
LsnPosition(lsn=...)LsnPositionStart reading from a specific Log Sequence Number (LSN).

Note: LsnPosition is for initial positioning only — it is distinct from the confirmed LSN that the connector sends to the server for WAL pruning.

Replication Slot Configuration

ParameterTypeDefaultDescription
replication_slotstrNoneNone
publication_namestrNoneNone
slot_behavior"create""reuse""reuse"

Advanced Configuration

CDC Output Format (cdc_format)

The cdc_format parameter controls how change data is structured in the output TableFrames, configured via CdcFormat.

from tabsdata.connector.cdc.common.typing import CdcFormat

cdc_format=CdcFormat(values_format="columns", flatten_values=True)
ParameterTypeDefaultDescription
values_format"columns""struct""map"
flatten_valuesboolTrueWhen True , new values are promoted to individual top-level columns instead of being packed into a container column.

Metadata columns (always present)

Every output row includes the following metadata columns regardless of values_format:

ColumnTypeDescription
@td.cdc.meta.opstrOperation type: "i" (insert), "u" (update), or "d" (delete).
@td.cdc.meta.txstrTransaction identifier from the source database.
@td.cdc.meta.sqintSequence number ordering changes within a transaction.

values_format = "columns"

Each source table column is represented as two explicit output columns — one for the old value and one for the new value:

ColumnDescription
@td.cdc.data.col.old.<COL_NAME>Value before the change.
@td.cdc.data.col.new.<COL_NAME>Value after the change. Present when flatten_values=False .
<COL_NAME>Value after the change. Present when flatten_values=True (replaces the new prefixed column).

Semantics by operation:

Operation@td.cdc.data.col.old.<COL_NAME>New value column
Insert ( i )nullInserted data
Update ( u )Value prior to the updateValue after the update
Delete ( d )nullDeleted data

values_format = "map"

Old and new values are packed into map columns keyed by table column name:

ColumnTypeDescription
@td.cdc.data.map.oldMap<str, str>Old values. Present when flatten_values=False or for old values always.
@td.cdc.data.map.newMap<str, str>New values packed as a map. Present when flatten_values=False .
<COL_NAME>New values as individual columns. Present when flatten_values=True (replaces @td.cdc.data.map.new ).

Semantics by operation:

Operation@td.cdc.data.map.oldNew value column(s)
Insert ( i )nullInserted data
Update ( u )Values prior to the updateValues after the update
Delete ( d )nullDeleted data

values_format = "struct"

Identical to "map" but old and new values are packed into struct fields instead of map columns:

ColumnTypeDescription
@td.cdc.data.row.oldstructOld values. Present when flatten_values=False or for old values always.
@td.cdc.data.row.newstructNew values packed as a struct. Present when flatten_values=False .
<COL_NAME>New values as individual columns. Present when flatten_values=True (replaces @td.cdc.data.row.new ).

Semantics by operation are identical to "map" above.

Output Examples

values_format="columns", flatten_values=True

@td.cdc.meta.op@td.cdc.meta.tx@td.cdc.meta.sq@td.cdc.meta.fmt@td.cdc.meta.flatidusernamefirst_namelast_nameemail@td.cdc.data.col.old.id@td.cdc.data.col.old.username@td.cdc.data.col.old.first_name@td.cdc.data.col.old.last_name@td.cdc.data.col.old.email
i225e1410-…:181columnstrue1deals_1914JohnnyWoodsreplaced1800@gmail.comnullnullnullnullnull
u225e1410-…:191columnstrue7filename_2073GerardoMcintoshsurgery1995@duck.com7filename_2073MarenPuckettexaminations2009@yahoo.com
d225e1410-…:201columnstrue2incl_1972EmeryReillyexposed2025@example.comnullnullnullnullnull

values_format="columns", flatten_values=False

@td.cdc.meta.op@td.cdc.meta.tx@td.cdc.meta.sq@td.cdc.meta.fmt@td.cdc.meta.flat@td.cdc.data.col.new.id@td.cdc.data.col.new.username@td.cdc.data.col.new.first_name@td.cdc.data.col.new.last_name@td.cdc.data.col.new.email@td.cdc.data.col.old.id@td.cdc.data.col.old.username@td.cdc.data.col.old.first_name@td.cdc.data.col.old.last_name@td.cdc.data.col.old.email
i225e1410-…:221columnsfalse1beat_1843KathyrnStokestrue1875@outlook.comnullnullnullnullnull
u225e1410-…:231columnsfalse7douglas_1901LawrenceBauersubmission2025@yahoo.com7douglas_1901HerminePrestoncommodities1921@outlook.com
d225e1410-…:241columnsfalse7douglas_1901LawrenceBauersubmission2025@yahoo.comnullnullnullnullnull

values_format="struct", flatten_values=True

@td.cdc.meta.op@td.cdc.meta.tx@td.cdc.meta.sq@td.cdc.meta.fmt@td.cdc.meta.flatidusernamefirst_namelast_nameemail@td.cdc.data.row.old
i225e1410-…:261structtrue1loops_1939AguedaDuncanclinical2027@protonmail.com{null,null,null,null,null}
u225e1410-…:271structtrue8evaluating_1979CarlettaDeleonwrapping1938@yandex.com{8,”evaluating_1979”,”Marlen”,”Estrada”,”hitachi1882@example.org”}
d225e1410-…:281structtrue4majority_1865EulahWhitneytouched1819@yahoo.com{null,null,null,null,null}

values_format="struct", flatten_values=False

@td.cdc.meta.op@td.cdc.meta.tx@td.cdc.meta.sq@td.cdc.meta.fmt@td.cdc.meta.flat@td.cdc.data.row.new@td.cdc.data.row.old
i225e1410-…:301structfalse{1,”processes_2081”,”Leon”,”Pollard”,”browse1909@duck.com”}{null,null,null,null,null}
u225e1410-…:311structfalse{5,”virtually_1823”,”Gavin”,”Macdonald”,”rocky2058@yandex.com”}{5,”virtually_1823”,”Erich”,”Hood”,”skin2004@gmail.com”}
d225e1410-…:321structfalse{7,”thank_1865”,”Lashawna”,”Petty”,”classical2074@yandex.com”}{null,null,null,null,null}

values_format="map", flatten_values=True

@td.cdc.meta.op@td.cdc.meta.tx@td.cdc.meta.sq@td.cdc.meta.fmt@td.cdc.meta.flatidusernamefirst_namelast_nameemail@td.cdc.data.map.old
i225e1410-…:341maptrue1uni_2028SandyHintonhusband1960@example.org{“id”:null,”username”:null,”first_name”:null,”last_name”:null,”email”:null}
u225e1410-…:351maptrue1uni_2028KelleNoelsee2021@example.com{“id”:1,”username”:”uni_2028”,”first_name”:”Sandy”,”last_name”:”Hinton”,”email”:”husband1960@example.org”}
d225e1410-…:361maptrue1uni_2028KelleNoelsee2021@example.com{“id”:null,”username”:null,”first_name”:null,”last_name”:null,”email”:null}

values_format="map", flatten_values=False

@td.cdc.meta.op@td.cdc.meta.tx@td.cdc.meta.sq@td.cdc.meta.fmt@td.cdc.meta.flat@td.cdc.data.map.new@td.cdc.data.map.old
ia4a17b92-…:381mapfalse{“id”:1,”username”:”vacancies_2045”,”first_name”:”Tony”,”last_name”:”Oliver”,”email”:”rec1977@yandex.com”}{“id”:null,”username”:null,”first_name”:null,”last_name”:null,”email”:null}
ua4a17b92-…:391mapfalse{“id”:7,”username”:”strategies_1852”,”first_name”:”Foster”,”last_name”:”Nolan”,”email”:”ambient1829@example.com”}{“id”:7,”username”:”strategies_1852”,”first_name”:”Doreatha”,”last_name”:”Mclaughlin”,”email”:”buffalo2065@yandex.com”}
da4a17b92-…:401mapfalse{“id”:8,”username”:”boc_1991”,”first_name”:”Peg”,”last_name”:”Vang”,”email”:”blacks1939@yandex.com”}{“id”:null,”username”:null,”first_name”:null,”last_name”:null,”email”:null}

Start Position Examples

from tabsdata.connector.cdc.postgres.typing import LsnPosition
from datetime import datetime, timezone

# Start from the end — capture only new changes going forward
start_from="tail"

# Start from the beginning of the WAL
start_from="head"

# Start from a specific LSN
start_from=LsnPosition(lsn=23456789)

Buffer and Trigger Thresholds

The CDC connector uses a two-stage pipeline: changes accumulate in memory (buffer), are flushed to the working directory, then staged to the output location.

Buffer thresholds (memory → working directory)

ParameterTypeDefaultDescription
buffer_max_rowsint10,000Flush to disk when row count in memory reaches this limit.
buffer_max_bytesintNoneNone
buffer_max_secfloat60.0Flush to disk when this many seconds have elapsed since the last flush.

Trigger thresholds (working directory → stage location)

ParameterTypeDefaultDescription
trigger_max_rowsintNoneNone
trigger_max_bytesintNoneNone
trigger_max_secfloat60.0Stage when this many seconds have elapsed since the last stage.

Other Parameters

ParameterTypeDefaultDescription
poll_interval_secfloat1.0Seconds between polling cycles when no new events are available.
blocking_timeout_secfloat1.0Timeout in seconds for blocking reads from the replication stream.
startdatetimeNoneNone
enddatetimeNoneNone

Limitations

  • TRUNCATE: TRUNCATE TABLE operations are not captured. A truncate on a tracked table will not produce any change events.
  • Large/Blob types: BLOB, CLOB, LONGBLOB, BYTEA, and TEXT (in some configurations) column types are not currently supported. Tables containing these types should exclude them from capture or use alternative ingestion methods.
  • Static table list: All tables in the tables parameter must exist before the trigger starts. The connector does not perform runtime table discovery.

PostgreSQL Setup to Enable CDC

The steps below are provided for convenience. Refer to the PostgreSQL documentation for comprehensive and up-to-date configuration instructions.

Server Configuration

Add the following to postgresql.conf and restart the server:

wal_level = logical
max_replication_slots = 4 # at least 1 per CDC consumer
max_wal_senders = 4 # at least 1 per CDC consumer

Install wal2json

The wal2json extension must be installed on the PostgreSQL server. The connector uses wal2json format version 2 by default, which produces a JSON object per tuple (row change) with optional transaction boundary markers.

On Debian/Ubuntu, wal2json is available as a system package:

# Adjust version number to match your PostgreSQL version
apt-get install postgresql-17-wal2json

For installation instructions and detailed documentation, see the wal2json project.

Create a CDC User

CREATE ROLE cdc_user WITH LOGIN REPLICATION PASSWORD 'cdc_password';
GRANT CONNECT ON DATABASE my_database TO cdc_user;
GRANT USAGE ON SCHEMA public TO cdc_user;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO cdc_user;

Set Replica Identity

For full before-image data on updates and deletes, set REPLICA IDENTITY FULL on each table you want to capture. Without this, only primary key columns are included in the before-image.

ALTER TABLE orders REPLICA IDENTITY FULL;
ALTER TABLE order_items REPLICA IDENTITY FULL;

Create a Replication Slot (optional)

You can pre-create a logical replication slot, or let the connector manage it via the slot_behavior parameter.

SELECT pg_create_logical_replication_slot('my_cdc_slot', 'wal2json');

Warning: Abandoned replication slots cause unbounded WAL growth. Monitor slot lag via pg_replication_slots and drop unused slots promptly.