ML data engineering
Batch, Change Data Capture, and Stream Ingestion
Compare ingestion patterns, delivery guarantees, replay, deduplication, and source responsibilities.
By the end you can
- Compare batch extracts, CDC, append-only events, and message streams
- Define delivery, ordering, replay, and deduplication responsibilities
- Identify ingestion failures that preserve plausible row counts
- Write an ingestion contract before selecting a tool
Example
Ingestion failures that preserve row counts
Volume checks alone can miss these correctness problems. The clearest public case is a load job whose output stayed plausible on every dashboard that watched it while thousands of records were simply absent.
Some lab result files were larger than the maximum file size the loader accepted, and what did not fit was never loaded. Public Health England published what that cost on 4 October 2020, in a statement by its interim chief executive, Michael Brodie: “A technical issue was identified overnight on Friday 2 October in the data load process that transfers COVID-19 positive lab results into reporting dashboards. After rapid investigation, we have identified that 15,841 cases between 25 September and 2 October were not included in the reported daily COVID-19 cases.” Over 75% of the missing cases — 11,968 of the 15,841 — were dated 30 September to 2 October alone.
The cost of the missing rows was afterwards measured rather than estimated in the abstract. A peer-reviewed natural experiment built on the failure, published in PNAS in 2021 by Fetzer and Graeber, found that one case referred late to contact tracing was associated with 18.6 additional infections and about 0.24 additional COVID-19 deaths over the following six weeks. The share of contacts reached within 24 hours fell from around 80% to just above 60%. The pipeline reported success throughout. The failure was in the loader, and the damage was in the six weeks that followed.
- Checkpoint gap: an incremental job advances its watermark before the sink commit and permanently skips records after a crash. The run is marked successful and the next run starts past the hole, so no count is ever short twice.
- Duplicate effect: a retried stream message increments a counter twice because the sink operation is not idempotent. Deduplication in the transport, such as Kafka's idempotent producer, protects the log and not your warehouse table.
- Lost delete: CDC captures inserts and updates but drops tombstones. In Kafka the delete marker is a record with a key and a null payload, and the documentation is explicit that the marker does not last: “This delete marker will cause any prior message with that key to be removed (as would any new message with that key), but delete markers are special in that they will themselves be cleaned out of the log after a period of time to free up space.” That period is delete.retention.ms, 86400000 ms — one day — by default, which the topic-config text says “gives a bound on the time in which a consumer must complete a read if they begin from offset 0”. Debezium emits a tombstone after every delete event, with tombstones.on.delete defaulting to true. Omit either half and closed accounts stay active in downstream state.
- Snapshot race: a CDC stream starts after a database snapshot without a shared source position, leaving a gap between the two. That is the precise problem Netflix's DBLog answers, by writing low and high watermarks into the transaction log and selecting a chunk of the table between them — 1,024 rows at a time in Debezium's default.
- Partition skew: one hot key delays a stream partition while aggregate throughput looks healthy.
Freshness is only one dimension of ingestion
Teams often frame ingestion as a choice between slow batch jobs and fast streams. That framing misses the harder questions about replay, updates, retries, and meaningful ordering.
A daily snapshot can be correct and sufficient for monthly forecasting. A stream can be low-latency and still unusable, if events lack stable IDs or arrive with inconsistent clocks.
The ingestion contract should describe records, delivery, ordering, correction, retention, and recovery. Latency matters, but historical reproducibility and failure behavior matter just as much.
ML workflows also need two views of time. They must know when the real event happened and when the platform learned about it, and knowing both shapes feature windows, labels, monitoring, and backfills. The distinction is not folklore. It was defined in 2015, in the paper that introduced the Dataflow Model, and the first of its two time domains reads: “Event Time, which is the time at which the event itself actually occurred, i.e. a record of system clock time (for whatever system generated the event) at the time of occurrence.” Processing time is when the pipeline observed it. The gap between the two is a dynamically changing skew, and a watermark tracks it. The same paper concedes the limit that makes a late-data policy necessary at all: for most real-world distributed data sets the system lacks sufficient knowledge to establish a 100% correct watermark, so most watermarks are heuristic. Apache Flink's documentation adopts the same two domains and says outright that it implements many techniques from the Dataflow Model.
Kafka’s own design document names the three delivery guarantees plainly and separately. At most once may lose messages but never redelivers. At least once never loses but may redeliver. Exactly once delivers each message once. Since version 0.11.0.0 the producer has an idempotent option. It works because “the broker assigns each producer an ID and deduplicates messages using a sequence number”. That is a property of the transport, not a promise about your sink.
Choose ingestion by the guarantees the consumer needs, not by whether streaming sounds more modern.
Comparison
Four common ingestion patterns
Each pattern can be reliable when its assumptions match the source and consumer. Change data capture carries the hardest coordination problem of the four: reconciling a full-state read with a live log. That problem has a published solution rather than a warning.
Netflix hit it and wrote up the fix. DBLog, published in 2020 by Andreakis and Papapanagiotou, states the idea in its abstract: “DBLog utilizes a watermark based approach that allows us to interleave transaction log events with rows that we directly select from tables to capture the full state.” Low and high watermarks go into the transaction log itself. A chunk of the table is selected between them. No locks are taken on the source.
Debezium adopted the same algorithm in version 1.6, and described the snapshot window and its buffer on the Debezium blog on 7 October 2021. In the MySQL connector the default incremental-snapshot chunk size is 1,024 rows. That write-up names the three problems the approach removes: snapshots that must run all-or-nothing, snapshots that cannot be resumed after a restart, and change streaming blocked until the snapshot finishes. Two independent systems now ship the mechanism. That is the difference between telling a team to coordinate snapshot and log, and pointing them at how it is done.
Full batch snapshot
Copy the current state on a schedule.
- Simple recovery because each run is self-contained
- Can be expensive for large sources
- May erase the history of intermediate changes
- Example: nightly reference catalog export
Incremental batch
Read records changed since a checkpoint.
- Reduces volume and supports scheduled freshness
- Depends on a reliable update marker
- Late corrections can fall behind the checkpoint
- Example: hourly orders by modification timestamp
Change data capture
Capture inserts, updates, and deletes from a database log.
- Preserves row-level mutations and deletion events
- Requires transaction-order and snapshot coordination
- Source schema changes need careful handling
- Example: replicate account state into an analytical store
Event stream
Publish domain events as actions occur.
- Supports low-latency and append-oriented processing
- Events may arrive late, duplicate, or out of order
- Requires replay retention and event-time policies
- Example: impressions and clicks for recommendations
Visual
Delivery is a chain of responsibilities
End-to-end behavior depends on producer IDs, transport, consumer state, and sink writes together. Steps 3 and 4 are where the guarantee is actually bought — making transformations repeatable across retries, and committing output without duplicate logical results. The bill for it has been published.
Google's MillWheel implements exactly-once by journaling a unique record ID in the same atomic write as the state modification, with a Bloom filter of record fingerprints as the fast path. Ten authors published it in 2013, and they measured what the machinery costs. On a single-stage pipeline running over 200 CPUs, the median record delay was 3.6 milliseconds and the 95th-percentile delay 30 milliseconds. Then comes the other half of the comparison: “This test was performed with strong productions and exactly-once disabled. With both of these features enabled, median latency jumps up to 33.7 milliseconds and 95th-percentile latency to 93.8 milliseconds.”
That is roughly a ninefold increase at the median, for a property most architecture diagrams draw as a single arrow between two boxes. The chain above is not free at any link. Steps 3 and 4 are where the cost lands.
1. Produce
Create a durable record with a stable identity and source sequence or timestamp.
2. Transport
Buffer, partition, retain, and redeliver records according to the messaging system.
3. Consume
Track progress and make transformations repeatable across retries.
4. Commit
Write output and checkpoint state without creating duplicate logical results.
5. Reconcile
Compare source and sink counts, gaps, deletions, and late corrections.
A transport guarantee alone cannot prove exactly-once business effects at the final dataset.
Steps
Design the ingestion contract before choosing infrastructure
A good design starts with source behavior and downstream consequences. Step 4 is the one teams write in the abstract and discover concretely, because the window you are allowed to replay belongs to systems the pipeline does not own.
Kafka's per-topic retention.ms defaults to 604800000 ms — seven days — and the documentation does not present it as a storage knob: “This represents an SLA on how soon consumers must read their data.” The delete markers that carry removals expire sooner still, after delete.retention.ms, 86400000 ms — one day — by default. On the source side, MySQL is typically configured to purge its binary logs after a set period. That is exactly why the Debezium MySQL connector must take an initial consistent snapshot before it can stream from the log at all.
So the safe recomputation boundary in step 4 is not a number the team chooses. It is the shortest of the retention windows the team inherits: seven days of transport by default, one day for deletes, and whatever is left on the binlog. Any backfill plan that reaches further back has to name the historical source it will read instead.
1. Set the freshness objective
State how late data may be before the ML use case is harmed.
2. Define change semantics
Specify inserts, updates, deletes, corrections, and immutable events.
3. Choose identity and ordering
Document deduplication keys, partitions, source positions, and event-time clocks.
4. Plan replay and backfill
Define retention, checkpoints, historical sources, and safe recomputation boundaries.
5. Reconcile end to end
Measure source-to-sink completeness, duplicate effects, delay, and correction handling.
Your replay window is the shortest retention you inherit, not the one you intended to have.
Key idea
“Exactly once” has a boundary
A stream processor may provide exactly-once state updates within its managed runtime. That does not automatically cover external APIs, databases, feature stores, or side effects beyond the transaction boundary.
Even when each event is applied once, the event itself may represent a duplicated real-world action. Conversely, two legitimate events can share an incorrectly reused identifier.
The Kafka documentation draws its own boundary in a single sentence. “Kafka supports exactly-once delivery in Kafka Streams”, reading, processing and writing on Kafka topics; a transactional producer paired with a read-committed consumer holds the same property on the same terms. Outside that scope the text hands the problem back rather than extending the promise: “Exactly-once delivery for other destination systems generally requires cooperation with such systems, but Kafka provides the primitives which makes implementing this feasible”. The classic answer for an external system is a two-phase commit between the offset and the output. Kafka’s own advice is simpler: store the offset in the same place as the output, as Connect does with HDFS.
Inside the boundary the guarantee is not free either, and the Kafka team measured that themselves. Their SIGMOD paper of 2021 reports a 10 to 20 percent throughput degradation for exactly-once against at-least-once, on a three-node cluster of i3.large EC2 nodes. In Bloomberg's production MxFlow deployment, running at 10,000 to 25,000 messages per second, the cost was 6% to 10%. Describe the actual boundary — exactly-once checkpointing, idempotent sink writes, transactional commits, or business-level deduplication. Then decide whether the throughput it costs inside that boundary is worth what it buys.
Exactly-once is a scope and a bill: name the boundary, then pay the 10 to 20 percent it costs inside it.
Analogy
Registered mail, and the ledger behind it
Registered mail travels under a tracking number. Distribution centers record its progress, and the recipient signs for delivery. At-least-once delivery resembles resending a letter when nobody has signed for the first one. Idempotent handling means the recipient does not act on the same letter twice. Replay retention is the archive used to resend a historical range — and the archive has an expiry date on it, seven days by default for a Kafka topic, one day for the notices that record a deletion.
One duplicate event travels into many aggregates, features, and labels at once. Deduplication may therefore need to happen before and after stateful computation, and not merely at the mailbox.
Reliable ingestion pairs durable identity and replay with idempotent state changes.
Key takeaways
- Batch, incremental extraction, CDC, and event streams trade simplicity, latency, mutation history, and recovery behavior differently. A batch loader that rejected oversized files kept 15,841 positive COVID-19 cases out of Public Health England's reported daily figures, and no count ever looked wrong.
- An ingestion contract should define delivery, identity, ordering, correction, retention, replay, and reconciliation. A delete, for instance, is a two-part contract: a keyed record with a null payload, read before delete.retention.ms expires at one day by default.
- Stable IDs and idempotent state changes are central to safe retries under at-least-once delivery. MillWheel bought exactly-once with a unique record ID journaled in the same atomic write as the state change, and paid a median record delay of 33.7 ms instead of 3.6 ms.
- Exactly-once claims are valid only within a named state and transaction boundary. Kafka's is Kafka-to-Kafka, and the Kafka team measured its cost inside that boundary at 10 to 20 percent throughput, 6% to 10% at Bloomberg.
- Watermarks estimate event-time progress. The Dataflow Model states that for most real-world distributed data sets there is not enough knowledge to establish a 100% correct one, which is why most watermarks are heuristic and late records still arrive.
- Recovery, replay, and backfill requirements often matter more than steady-state throughput. Kafka frames its seven-day default retention as an SLA on how soon consumers must read their data, and a purged MySQL binlog is why Debezium must snapshot before it can stream.