Cursor-based sync vs. Change Data Capture: what your data movement tool is missing
Cursor-based syncs miss changes between cycles. Learn how log-based Change Data Capture solves this without relying on streaming



Every data movement tool promises the same thing: to keep your warehouse in sync with whatever happens in your production database. How this is done under the hood, however, completely changes how much you can actually trust the data arriving on the other side.
Most tools on the market, including older versions of connectors you've probably used, solve this through cursor-based sync. It works, until it doesn't, and the reason it stops working almost never shows up as an error.
What cursor sync actually is in practice
The logic is simple to explain. The tool looks at a column in your table, typically something like updated_at, stores the highest value it saw in a previous run (the cursor), and in the next execution queries only for records with a value greater than that. Something like:
sql
SELECT * FROM leads WHERE updated_at > '2026-07-27 09:00:00'
If it finds records, it syncs them and advances the cursor to the new maximum value. If it doesn't, it does nothing until the next run. It is cheap to run, simple to implement, and works well under one specific condition: the table must have been designed with this in mind, and the data modification pattern needs to be simple enough to fit into a single timestamp column.
In practice, three things break this condition more often than you might think.
Issue 1: Not every table has a reliable update column
A good chunk of tables in production don't have a reliable update column, or don't have one at all. Someone designed that schema thinking about the application relying on it, not about the person who would consume this data later for analytics, and that is absolutely reasonable from the developer's perspective.
The problem appears on the other side. Without a column that safely tracks when a record changed, there is no reliable way to know what to sync incrementally. The only remaining option is a full refresh: re-reading the entire table on every run to ensure nothing is left behind. This costs processing time, warehouse compute resources, and for large tables, it simply might not fit into your available sync window. This is the most common reason a data team gives up on syncing a specific table—not because it doesn't matter, but because the chosen method just can't handle it.
Issue 2: Deletes are invisible to cursor-based sync
This is the issue that is least discussed, and it's structural, not a configuration problem. Cursor sync queries for records with an updated_at greater than the last seen value. A DELETE doesn't update any column; it removes the row. There is no "this was deleted at 14:32pm" timestamp for a cursor query to find, because the row simply isn't there to be read anymore.
In practice, this means deleted source records linger forever in your warehouse, unless someone runs a full reconciliation periodically to compare source and destination row counts. Teams unaware of this find out the hard way: a report counting active customers or valid orders will include records that were canceled or removed months ago at the source.
Issue 3: Intermediate states vanish between sync cycles if you need history
Even when the column exists, is accurate, and no deletes are involved, the cursor only sees one thing: the state of the record at the exact millisecond the query ran. If that same record changed two or three times between runs, the next execution only captures the most recent value.
This really matters when your use case requires an append-only mode in the warehouse, keeping every change as a historical row instead of just overwriting the current state. If you only need to know the latest state of a record, cursor-based sync still delivers that. The problem arises when you need to reconstruct the full journey, and everything that happened between cycles is simply lost.
It's not a bug; it's just the mechanics of the method. A snapshot taken at every cycle only shows what existed at the time of the snapshot. This normally doesn't trigger a system error. Instead, it shows up as an unanswered question: why doesn't this number match what the sales or support team is seeing live in the source app? And the person answering that question is usually not the one who designed the pipeline in the first place.
How Change Data Capture changes things
Change Data Capture (CDC) tackles the problem from a different angle. Instead of asking the database "what has changed since last time", it reads directly from the transaction log that the database already keeps to guarantee integrity and crash recovery. In PostgreSQL, this log is the WAL (Write Ahead Log): before any change is applied to the tables, it is logged in order, with full details of the operation, including inserts, updates, and deletes.
A CDC process streams this log continuously through a logical replication slot, knowing exactly what changed and when, operation by operation, without relying on any special column or comparing snapshots. This solves all three issues at once:
Tables without update columns are no longer blockers, because the method doesn't depend on them.
Deletes are no longer invisible, because removals are also explicitly logged operations, not just the absence of a row.
No intermediate states are lost between sync cycles, because every change is captured individually when it happens, rather than being inferred later. This works whether you want to maintain only the latest state (overwrite) or need the full row-by-row history (append).
What this requires from your database
It's worth being honest about what you give up in exchange for this reliability, because it is not zero-configuration.
In PostgreSQL, the wal_level must be set to logical, you need to set up enough replication slots and WAL senders (max_replication_slots and max_wal_senders), and the tables you want to capture need their replica identity set to FULL so updates and deletes carry the complete state of the affected record, not just the primary key. This increases the volume of WAL generated by the database, so it's wise to configure a retention limit on the replication slot (max_slot_wal_keep_size) so a temporary connection failure doesn't end up filling your database disk with accumulated WALs waiting to be consumed.
The initial connection also requires deciding between two modes: an initial full snapshot of the table followed by streaming changes from that point forward (initial), or starting directly from what is currently available in the WAL without pulling historical data (no_data). The right choice depends on whether or not you need the existing state of the table as a starting point.
None of this is a reason to avoid CDC, but it is a reason not to treat it as a trivial config flip. It's best to plan this with the DBA or infra team before enabling it in production.
CDC is not synonymous with real-time streaming, and that is by design
There is a common misconception in the industry: because CDC captures changes in real-time, many assume the end goal must also be real-time delivery, requiring a full streaming infrastructure. That's not quite true, and it's worth separating the two concepts.
Capturing every change as it happens at the source is about data integrity, not about ingestion speed. What you do with those changes afterward is a separate decision, and it should be driven by whoever actually consumes the data, not by the technical capabilities of your replication tool.
Think about how many of your data consumers actually process changes the second they happen. A BI dashboard, for instance, updates at most a few times a day, and in practice is looked at even less frequently. Analytical models, executive reports, and most operational analytics work perfectly fine in cycles of minutes, not milliseconds. Paying for a full streaming infrastructure to power something that is only read once or twice a day introduces unnecessary complexity and costs that your team won't actually benefit from.
The most sensible architecture choice for most data teams is not between fast and slow. It's between reliable and unreliable. Log-based CDC solves the reliability part, ensuring that no change is lost at the source—including deletes and intermediate states. The load frequency to your warehouse can, and in most cases should, remain a predictable and cost-effective batch cycle, not forced streaming.
When cursor-based sync still makes sense
To be equally fair: not every table needs CDC. If the table is small, has a reliable update column, never undergoes hard deletes (only soft deletes with a flag, for example), and you only need the latest state (overwrite), cursor sync remains a lightweight option with less database configuration. CDC pays off exactly where cursors fail: large tables, hard deletes, append-only history needs, or where the lack of an updated column is currently preventing you from syncing that table incrementally.
Wrapping Up
If your current data movement tool relies on an update column your table doesn't have, if source deletes are still alive in your warehouse, or if you've struggled to explain why numbers don't match between systems, the problem probably isn't your pipeline. It's the replication method.
The Erathos PostgreSQL connector supports WAL-based CDC for both overwrite and append sync modes. You can find the complete configuration steps and database adjustment details in our technical documentation: docs.erathos.com/connectors/databases/postgresql
If you'd rather test it out on your own database first, you can create a free account and connect a Postgres table with CDC enabled in just a few minutes, no credit card required.
Every data movement tool promises the same thing: to keep your warehouse in sync with whatever happens in your production database. How this is done under the hood, however, completely changes how much you can actually trust the data arriving on the other side.
Most tools on the market, including older versions of connectors you've probably used, solve this through cursor-based sync. It works, until it doesn't, and the reason it stops working almost never shows up as an error.
What cursor sync actually is in practice
The logic is simple to explain. The tool looks at a column in your table, typically something like updated_at, stores the highest value it saw in a previous run (the cursor), and in the next execution queries only for records with a value greater than that. Something like:
sql
SELECT * FROM leads WHERE updated_at > '2026-07-27 09:00:00'
If it finds records, it syncs them and advances the cursor to the new maximum value. If it doesn't, it does nothing until the next run. It is cheap to run, simple to implement, and works well under one specific condition: the table must have been designed with this in mind, and the data modification pattern needs to be simple enough to fit into a single timestamp column.
In practice, three things break this condition more often than you might think.
Issue 1: Not every table has a reliable update column
A good chunk of tables in production don't have a reliable update column, or don't have one at all. Someone designed that schema thinking about the application relying on it, not about the person who would consume this data later for analytics, and that is absolutely reasonable from the developer's perspective.
The problem appears on the other side. Without a column that safely tracks when a record changed, there is no reliable way to know what to sync incrementally. The only remaining option is a full refresh: re-reading the entire table on every run to ensure nothing is left behind. This costs processing time, warehouse compute resources, and for large tables, it simply might not fit into your available sync window. This is the most common reason a data team gives up on syncing a specific table—not because it doesn't matter, but because the chosen method just can't handle it.
Issue 2: Deletes are invisible to cursor-based sync
This is the issue that is least discussed, and it's structural, not a configuration problem. Cursor sync queries for records with an updated_at greater than the last seen value. A DELETE doesn't update any column; it removes the row. There is no "this was deleted at 14:32pm" timestamp for a cursor query to find, because the row simply isn't there to be read anymore.
In practice, this means deleted source records linger forever in your warehouse, unless someone runs a full reconciliation periodically to compare source and destination row counts. Teams unaware of this find out the hard way: a report counting active customers or valid orders will include records that were canceled or removed months ago at the source.
Issue 3: Intermediate states vanish between sync cycles if you need history
Even when the column exists, is accurate, and no deletes are involved, the cursor only sees one thing: the state of the record at the exact millisecond the query ran. If that same record changed two or three times between runs, the next execution only captures the most recent value.
This really matters when your use case requires an append-only mode in the warehouse, keeping every change as a historical row instead of just overwriting the current state. If you only need to know the latest state of a record, cursor-based sync still delivers that. The problem arises when you need to reconstruct the full journey, and everything that happened between cycles is simply lost.
It's not a bug; it's just the mechanics of the method. A snapshot taken at every cycle only shows what existed at the time of the snapshot. This normally doesn't trigger a system error. Instead, it shows up as an unanswered question: why doesn't this number match what the sales or support team is seeing live in the source app? And the person answering that question is usually not the one who designed the pipeline in the first place.
How Change Data Capture changes things
Change Data Capture (CDC) tackles the problem from a different angle. Instead of asking the database "what has changed since last time", it reads directly from the transaction log that the database already keeps to guarantee integrity and crash recovery. In PostgreSQL, this log is the WAL (Write Ahead Log): before any change is applied to the tables, it is logged in order, with full details of the operation, including inserts, updates, and deletes.
A CDC process streams this log continuously through a logical replication slot, knowing exactly what changed and when, operation by operation, without relying on any special column or comparing snapshots. This solves all three issues at once:
Tables without update columns are no longer blockers, because the method doesn't depend on them.
Deletes are no longer invisible, because removals are also explicitly logged operations, not just the absence of a row.
No intermediate states are lost between sync cycles, because every change is captured individually when it happens, rather than being inferred later. This works whether you want to maintain only the latest state (overwrite) or need the full row-by-row history (append).
What this requires from your database
It's worth being honest about what you give up in exchange for this reliability, because it is not zero-configuration.
In PostgreSQL, the wal_level must be set to logical, you need to set up enough replication slots and WAL senders (max_replication_slots and max_wal_senders), and the tables you want to capture need their replica identity set to FULL so updates and deletes carry the complete state of the affected record, not just the primary key. This increases the volume of WAL generated by the database, so it's wise to configure a retention limit on the replication slot (max_slot_wal_keep_size) so a temporary connection failure doesn't end up filling your database disk with accumulated WALs waiting to be consumed.
The initial connection also requires deciding between two modes: an initial full snapshot of the table followed by streaming changes from that point forward (initial), or starting directly from what is currently available in the WAL without pulling historical data (no_data). The right choice depends on whether or not you need the existing state of the table as a starting point.
None of this is a reason to avoid CDC, but it is a reason not to treat it as a trivial config flip. It's best to plan this with the DBA or infra team before enabling it in production.
CDC is not synonymous with real-time streaming, and that is by design
There is a common misconception in the industry: because CDC captures changes in real-time, many assume the end goal must also be real-time delivery, requiring a full streaming infrastructure. That's not quite true, and it's worth separating the two concepts.
Capturing every change as it happens at the source is about data integrity, not about ingestion speed. What you do with those changes afterward is a separate decision, and it should be driven by whoever actually consumes the data, not by the technical capabilities of your replication tool.
Think about how many of your data consumers actually process changes the second they happen. A BI dashboard, for instance, updates at most a few times a day, and in practice is looked at even less frequently. Analytical models, executive reports, and most operational analytics work perfectly fine in cycles of minutes, not milliseconds. Paying for a full streaming infrastructure to power something that is only read once or twice a day introduces unnecessary complexity and costs that your team won't actually benefit from.
The most sensible architecture choice for most data teams is not between fast and slow. It's between reliable and unreliable. Log-based CDC solves the reliability part, ensuring that no change is lost at the source—including deletes and intermediate states. The load frequency to your warehouse can, and in most cases should, remain a predictable and cost-effective batch cycle, not forced streaming.
When cursor-based sync still makes sense
To be equally fair: not every table needs CDC. If the table is small, has a reliable update column, never undergoes hard deletes (only soft deletes with a flag, for example), and you only need the latest state (overwrite), cursor sync remains a lightweight option with less database configuration. CDC pays off exactly where cursors fail: large tables, hard deletes, append-only history needs, or where the lack of an updated column is currently preventing you from syncing that table incrementally.
Wrapping Up
If your current data movement tool relies on an update column your table doesn't have, if source deletes are still alive in your warehouse, or if you've struggled to explain why numbers don't match between systems, the problem probably isn't your pipeline. It's the replication method.
The Erathos PostgreSQL connector supports WAL-based CDC for both overwrite and append sync modes. You can find the complete configuration steps and database adjustment details in our technical documentation: docs.erathos.com/connectors/databases/postgresql
If you'd rather test it out on your own database first, you can create a free account and connect a Postgres table with CDC enabled in just a few minutes, no credit card required.
Every data movement tool promises the same thing: to keep your warehouse in sync with whatever happens in your production database. How this is done under the hood, however, completely changes how much you can actually trust the data arriving on the other side.
Most tools on the market, including older versions of connectors you've probably used, solve this through cursor-based sync. It works, until it doesn't, and the reason it stops working almost never shows up as an error.
What cursor sync actually is in practice
The logic is simple to explain. The tool looks at a column in your table, typically something like updated_at, stores the highest value it saw in a previous run (the cursor), and in the next execution queries only for records with a value greater than that. Something like:
sql
SELECT * FROM leads WHERE updated_at > '2026-07-27 09:00:00'
If it finds records, it syncs them and advances the cursor to the new maximum value. If it doesn't, it does nothing until the next run. It is cheap to run, simple to implement, and works well under one specific condition: the table must have been designed with this in mind, and the data modification pattern needs to be simple enough to fit into a single timestamp column.
In practice, three things break this condition more often than you might think.
Issue 1: Not every table has a reliable update column
A good chunk of tables in production don't have a reliable update column, or don't have one at all. Someone designed that schema thinking about the application relying on it, not about the person who would consume this data later for analytics, and that is absolutely reasonable from the developer's perspective.
The problem appears on the other side. Without a column that safely tracks when a record changed, there is no reliable way to know what to sync incrementally. The only remaining option is a full refresh: re-reading the entire table on every run to ensure nothing is left behind. This costs processing time, warehouse compute resources, and for large tables, it simply might not fit into your available sync window. This is the most common reason a data team gives up on syncing a specific table—not because it doesn't matter, but because the chosen method just can't handle it.
Issue 2: Deletes are invisible to cursor-based sync
This is the issue that is least discussed, and it's structural, not a configuration problem. Cursor sync queries for records with an updated_at greater than the last seen value. A DELETE doesn't update any column; it removes the row. There is no "this was deleted at 14:32pm" timestamp for a cursor query to find, because the row simply isn't there to be read anymore.
In practice, this means deleted source records linger forever in your warehouse, unless someone runs a full reconciliation periodically to compare source and destination row counts. Teams unaware of this find out the hard way: a report counting active customers or valid orders will include records that were canceled or removed months ago at the source.
Issue 3: Intermediate states vanish between sync cycles if you need history
Even when the column exists, is accurate, and no deletes are involved, the cursor only sees one thing: the state of the record at the exact millisecond the query ran. If that same record changed two or three times between runs, the next execution only captures the most recent value.
This really matters when your use case requires an append-only mode in the warehouse, keeping every change as a historical row instead of just overwriting the current state. If you only need to know the latest state of a record, cursor-based sync still delivers that. The problem arises when you need to reconstruct the full journey, and everything that happened between cycles is simply lost.
It's not a bug; it's just the mechanics of the method. A snapshot taken at every cycle only shows what existed at the time of the snapshot. This normally doesn't trigger a system error. Instead, it shows up as an unanswered question: why doesn't this number match what the sales or support team is seeing live in the source app? And the person answering that question is usually not the one who designed the pipeline in the first place.
How Change Data Capture changes things
Change Data Capture (CDC) tackles the problem from a different angle. Instead of asking the database "what has changed since last time", it reads directly from the transaction log that the database already keeps to guarantee integrity and crash recovery. In PostgreSQL, this log is the WAL (Write Ahead Log): before any change is applied to the tables, it is logged in order, with full details of the operation, including inserts, updates, and deletes.
A CDC process streams this log continuously through a logical replication slot, knowing exactly what changed and when, operation by operation, without relying on any special column or comparing snapshots. This solves all three issues at once:
Tables without update columns are no longer blockers, because the method doesn't depend on them.
Deletes are no longer invisible, because removals are also explicitly logged operations, not just the absence of a row.
No intermediate states are lost between sync cycles, because every change is captured individually when it happens, rather than being inferred later. This works whether you want to maintain only the latest state (overwrite) or need the full row-by-row history (append).
What this requires from your database
It's worth being honest about what you give up in exchange for this reliability, because it is not zero-configuration.
In PostgreSQL, the wal_level must be set to logical, you need to set up enough replication slots and WAL senders (max_replication_slots and max_wal_senders), and the tables you want to capture need their replica identity set to FULL so updates and deletes carry the complete state of the affected record, not just the primary key. This increases the volume of WAL generated by the database, so it's wise to configure a retention limit on the replication slot (max_slot_wal_keep_size) so a temporary connection failure doesn't end up filling your database disk with accumulated WALs waiting to be consumed.
The initial connection also requires deciding between two modes: an initial full snapshot of the table followed by streaming changes from that point forward (initial), or starting directly from what is currently available in the WAL without pulling historical data (no_data). The right choice depends on whether or not you need the existing state of the table as a starting point.
None of this is a reason to avoid CDC, but it is a reason not to treat it as a trivial config flip. It's best to plan this with the DBA or infra team before enabling it in production.
CDC is not synonymous with real-time streaming, and that is by design
There is a common misconception in the industry: because CDC captures changes in real-time, many assume the end goal must also be real-time delivery, requiring a full streaming infrastructure. That's not quite true, and it's worth separating the two concepts.
Capturing every change as it happens at the source is about data integrity, not about ingestion speed. What you do with those changes afterward is a separate decision, and it should be driven by whoever actually consumes the data, not by the technical capabilities of your replication tool.
Think about how many of your data consumers actually process changes the second they happen. A BI dashboard, for instance, updates at most a few times a day, and in practice is looked at even less frequently. Analytical models, executive reports, and most operational analytics work perfectly fine in cycles of minutes, not milliseconds. Paying for a full streaming infrastructure to power something that is only read once or twice a day introduces unnecessary complexity and costs that your team won't actually benefit from.
The most sensible architecture choice for most data teams is not between fast and slow. It's between reliable and unreliable. Log-based CDC solves the reliability part, ensuring that no change is lost at the source—including deletes and intermediate states. The load frequency to your warehouse can, and in most cases should, remain a predictable and cost-effective batch cycle, not forced streaming.
When cursor-based sync still makes sense
To be equally fair: not every table needs CDC. If the table is small, has a reliable update column, never undergoes hard deletes (only soft deletes with a flag, for example), and you only need the latest state (overwrite), cursor sync remains a lightweight option with less database configuration. CDC pays off exactly where cursors fail: large tables, hard deletes, append-only history needs, or where the lack of an updated column is currently preventing you from syncing that table incrementally.
Wrapping Up
If your current data movement tool relies on an update column your table doesn't have, if source deletes are still alive in your warehouse, or if you've struggled to explain why numbers don't match between systems, the problem probably isn't your pipeline. It's the replication method.
The Erathos PostgreSQL connector supports WAL-based CDC for both overwrite and append sync modes. You can find the complete configuration steps and database adjustment details in our technical documentation: docs.erathos.com/connectors/databases/postgresql
If you'd rather test it out on your own database first, you can create a free account and connect a Postgres table with CDC enabled in just a few minutes, no credit card required.
Every data movement tool promises the same thing: to keep your warehouse in sync with whatever happens in your production database. How this is done under the hood, however, completely changes how much you can actually trust the data arriving on the other side.
Most tools on the market, including older versions of connectors you've probably used, solve this through cursor-based sync. It works, until it doesn't, and the reason it stops working almost never shows up as an error.
What cursor sync actually is in practice
The logic is simple to explain. The tool looks at a column in your table, typically something like updated_at, stores the highest value it saw in a previous run (the cursor), and in the next execution queries only for records with a value greater than that. Something like:
sql
SELECT * FROM leads WHERE updated_at > '2026-07-27 09:00:00'
If it finds records, it syncs them and advances the cursor to the new maximum value. If it doesn't, it does nothing until the next run. It is cheap to run, simple to implement, and works well under one specific condition: the table must have been designed with this in mind, and the data modification pattern needs to be simple enough to fit into a single timestamp column.
In practice, three things break this condition more often than you might think.
Issue 1: Not every table has a reliable update column
A good chunk of tables in production don't have a reliable update column, or don't have one at all. Someone designed that schema thinking about the application relying on it, not about the person who would consume this data later for analytics, and that is absolutely reasonable from the developer's perspective.
The problem appears on the other side. Without a column that safely tracks when a record changed, there is no reliable way to know what to sync incrementally. The only remaining option is a full refresh: re-reading the entire table on every run to ensure nothing is left behind. This costs processing time, warehouse compute resources, and for large tables, it simply might not fit into your available sync window. This is the most common reason a data team gives up on syncing a specific table—not because it doesn't matter, but because the chosen method just can't handle it.
Issue 2: Deletes are invisible to cursor-based sync
This is the issue that is least discussed, and it's structural, not a configuration problem. Cursor sync queries for records with an updated_at greater than the last seen value. A DELETE doesn't update any column; it removes the row. There is no "this was deleted at 14:32pm" timestamp for a cursor query to find, because the row simply isn't there to be read anymore.
In practice, this means deleted source records linger forever in your warehouse, unless someone runs a full reconciliation periodically to compare source and destination row counts. Teams unaware of this find out the hard way: a report counting active customers or valid orders will include records that were canceled or removed months ago at the source.
Issue 3: Intermediate states vanish between sync cycles if you need history
Even when the column exists, is accurate, and no deletes are involved, the cursor only sees one thing: the state of the record at the exact millisecond the query ran. If that same record changed two or three times between runs, the next execution only captures the most recent value.
This really matters when your use case requires an append-only mode in the warehouse, keeping every change as a historical row instead of just overwriting the current state. If you only need to know the latest state of a record, cursor-based sync still delivers that. The problem arises when you need to reconstruct the full journey, and everything that happened between cycles is simply lost.
It's not a bug; it's just the mechanics of the method. A snapshot taken at every cycle only shows what existed at the time of the snapshot. This normally doesn't trigger a system error. Instead, it shows up as an unanswered question: why doesn't this number match what the sales or support team is seeing live in the source app? And the person answering that question is usually not the one who designed the pipeline in the first place.
How Change Data Capture changes things
Change Data Capture (CDC) tackles the problem from a different angle. Instead of asking the database "what has changed since last time", it reads directly from the transaction log that the database already keeps to guarantee integrity and crash recovery. In PostgreSQL, this log is the WAL (Write Ahead Log): before any change is applied to the tables, it is logged in order, with full details of the operation, including inserts, updates, and deletes.
A CDC process streams this log continuously through a logical replication slot, knowing exactly what changed and when, operation by operation, without relying on any special column or comparing snapshots. This solves all three issues at once:
Tables without update columns are no longer blockers, because the method doesn't depend on them.
Deletes are no longer invisible, because removals are also explicitly logged operations, not just the absence of a row.
No intermediate states are lost between sync cycles, because every change is captured individually when it happens, rather than being inferred later. This works whether you want to maintain only the latest state (overwrite) or need the full row-by-row history (append).
What this requires from your database
It's worth being honest about what you give up in exchange for this reliability, because it is not zero-configuration.
In PostgreSQL, the wal_level must be set to logical, you need to set up enough replication slots and WAL senders (max_replication_slots and max_wal_senders), and the tables you want to capture need their replica identity set to FULL so updates and deletes carry the complete state of the affected record, not just the primary key. This increases the volume of WAL generated by the database, so it's wise to configure a retention limit on the replication slot (max_slot_wal_keep_size) so a temporary connection failure doesn't end up filling your database disk with accumulated WALs waiting to be consumed.
The initial connection also requires deciding between two modes: an initial full snapshot of the table followed by streaming changes from that point forward (initial), or starting directly from what is currently available in the WAL without pulling historical data (no_data). The right choice depends on whether or not you need the existing state of the table as a starting point.
None of this is a reason to avoid CDC, but it is a reason not to treat it as a trivial config flip. It's best to plan this with the DBA or infra team before enabling it in production.
CDC is not synonymous with real-time streaming, and that is by design
There is a common misconception in the industry: because CDC captures changes in real-time, many assume the end goal must also be real-time delivery, requiring a full streaming infrastructure. That's not quite true, and it's worth separating the two concepts.
Capturing every change as it happens at the source is about data integrity, not about ingestion speed. What you do with those changes afterward is a separate decision, and it should be driven by whoever actually consumes the data, not by the technical capabilities of your replication tool.
Think about how many of your data consumers actually process changes the second they happen. A BI dashboard, for instance, updates at most a few times a day, and in practice is looked at even less frequently. Analytical models, executive reports, and most operational analytics work perfectly fine in cycles of minutes, not milliseconds. Paying for a full streaming infrastructure to power something that is only read once or twice a day introduces unnecessary complexity and costs that your team won't actually benefit from.
The most sensible architecture choice for most data teams is not between fast and slow. It's between reliable and unreliable. Log-based CDC solves the reliability part, ensuring that no change is lost at the source—including deletes and intermediate states. The load frequency to your warehouse can, and in most cases should, remain a predictable and cost-effective batch cycle, not forced streaming.
When cursor-based sync still makes sense
To be equally fair: not every table needs CDC. If the table is small, has a reliable update column, never undergoes hard deletes (only soft deletes with a flag, for example), and you only need the latest state (overwrite), cursor sync remains a lightweight option with less database configuration. CDC pays off exactly where cursors fail: large tables, hard deletes, append-only history needs, or where the lack of an updated column is currently preventing you from syncing that table incrementally.
Wrapping Up
If your current data movement tool relies on an update column your table doesn't have, if source deletes are still alive in your warehouse, or if you've struggled to explain why numbers don't match between systems, the problem probably isn't your pipeline. It's the replication method.
The Erathos PostgreSQL connector supports WAL-based CDC for both overwrite and append sync modes. You can find the complete configuration steps and database adjustment details in our technical documentation: docs.erathos.com/connectors/databases/postgresql
If you'd rather test it out on your own database first, you can create a free account and connect a Postgres table with CDC enabled in just a few minutes, no credit card required.