Debezium Event Replay That Rewrites a Lead Engineer’s Rollback Plan by Record Age
The on-call page hit at 9:47 PM on a Thursday. A schema migration had been rolled out earlier that evening, adding a nullable column to the orders table. Within minutes, the downstream read-model cache that powered the company's core product began returning stale data. The lead engineer, who had written the rollback plan in a shared doc the day before, knew the standard playbook: drain the Kafka topic, re-snapshot the source database, and let downstream consumers rebuild their state. Estimated downtime: four to six hours. The team scheduled the rollback for Saturday morning, hoping to be done by lunch.
But as the migration rolled out, a subtle incompatibility between the new schema and an older consumer version triggered cascading failures. The read-model cache that powered the company's core product began returning stale data. Within twenty minutes, the on-call engineer paged the lead. The rollback plan was now live—but it was about to reveal a hidden bottleneck that had been invisible in all the pre-migration reviews.
That bottleneck was record age. The default Debezium replay strategy treats all events as equal, replaying them in partition order regardless of when they were produced. For a pipeline ingesting change events from a dozen tables over 48 hours, that meant every event—from minutes-old inserts to stale updates from yesterday—would be reprocessed in order. The downstream caches would be thrashed with writes for events that were already irrelevant. The lead engineer realized the plan needed a fundamental rewrite.
A Rollback That Could Have Cost a Weekend
The original rollback script was straightforward: stop the consumers, reset the Kafka consumer group offset to the earliest available message for the topic, and restart. Because the topic was compacted, the earliest message for each key represented the latest state before the schema change. But compacted topics do not guarantee a clean cutoff by timestamp. The lead engineer had planned to drain the topic entirely and trigger a fresh Debezium snapshot, which reads the entire table and emits a create event for every row.
For a set of 12 tables, with the largest table containing approximately 350 million rows and the total across all tables around 800 million rows, the full snapshot was estimated at four to six hours. That estimate assumed no upstream load on the source database and no downstream write amplification. In practice, the database was under normal production load, and each replayed event triggered cache updates, secondary index writes, and webhook calls. The write amplification factor was somewhere between 3x and 5x, meaning the downstream systems would see 12 to 30 hours worth of work compressed into a few hours.
The lead engineer had seen this pattern before. In a previous incident at a different company, a full re-snapshot of a 500-million-row table had taken over eight hours and caused a cascading timeout in a dependent service. She knew that the weekend plan was fragile, but she had no better alternative in her toolkit. The industry-standard advice for Debezium rollbacks was, and still is, to re-snapshot. No one had published a pattern for selective replay based on record age.
As she stared at the monitoring dashboards, watching the event lag climb, she made a decision: she would not follow the script. Instead, she would build a new one, live, during the incident. The stakes were high—every extra minute of downtime cost roughly $15,000 in lost transactions—but the alternative was a full weekend of pain.
Why Naive Event Replay Fails Under Backpressure
To understand why the naive approach fails, we need to look at how Kafka offset reset works. When a consumer group resets its offset to earliest, it starts reading from the oldest message in the partition that is still within the retention period. For a compacted topic, the oldest message may be days or weeks old. Debezium's change events carry a timestamp in the payload—the ts_ms field—but Kafka itself does not use that timestamp for offset management. The consumer has no built-in way to say, skip everything older than two hours ago.
Debezium's snapshot mode offers a different path: stop the connector, delete the offset topic, and restart with snapshot.mode: initial. This triggers a full table scan. The scan is efficient for small tables but becomes a bottleneck when tables are large or the source database is under load. Moreover, the snapshot does not filter by table; it snapshots all tables configured for the connector. If only one table's schema changed, the other eleven tables are re-snapped unnecessarily.
The lead engineer's original plan—drain the topic, re-snapshot—was essentially the same as the default Debezium rollback. But she had seen the write amplification problem before. Each replayed event from the snapshot would hit the downstream read-model cache, which stored aggregated views computed from multiple tables. A single row change could trigger recomputation of several cache keys. In the worst case, a single event could cause a cascade of cache invalidations that took seconds to settle.
Backpressure built quickly. The downstream consumers, written in a synchronous event-processing style, could not keep up with the replay rate. Their processing lag grew, and the system entered a feedback loop: the slower they processed, the more events piled up in the topic, increasing the lag further. The lead engineer estimated that at the current rate, the replay would take not four to six hours, but closer to ten to twelve. The weekend plan was dead.
The Record-Age Rewrite: Filtering Before Replay
The insight that saved the weekend was simple: not all events are equally valuable for recovery. Events older than a certain threshold represent state that is likely already reflected in the downstream system, either through prior processing or through idempotent updates. Replaying them only adds load. The goal was to replay only the events that were younger than, say, two hours—enough to cover the schema change window and any lag from the database replicas.
The implementation used a Kafka Streams application as a sidecar consumer. It read from the compacted Debezium topic, examined the ts_ms field in each event's payload, and filtered out events whose timestamp was older than a configurable cutoff. The remaining events were written to a staging topic. The downstream consumers then switched to reading from the staging topic, which contained only the relevant recent events. The filter ran as a stateless operation, so it could keep up with production throughput.
The cutoff was configurable at deployment time, typically set to two hours before the incident start. This allowed a safety margin for events that might have been delayed due to network partitions or database replica lag. The team also added a monitoring metric: the age of the oldest event still in the staging topic. If that age exceeded the cutoff plus a buffer, an alert fired, indicating that the filter was dropping events that might be needed.
The result was dramatic. Instead of replaying 48 hours of events, the system replayed roughly 30 minutes' worth. The staging topic filled and drained in under 30 minutes. Downstream caches warmed up quickly, and the read-model returned to consistency within an hour of the schema change being rolled back. The lead engineer documented the pattern in a runbook that same afternoon.
Tuning the Cutoff: Tradeoffs and Hedge Numbers
Choosing the right cutoff value is the central tuning challenge. Set it too short, and you risk missing late-arriving events from lagging database replicas. In a typical Debezium deployment, the connector reads from the primary database's binlog, but replicas may lag by seconds or minutes. If the schema change was applied to the primary but the connector's offset was slightly behind, some events from the replica could arrive after the cutoff. Those events would be dropped, potentially causing data inconsistency.
Set it too long, and you negate the time savings. A cutoff of six hours might still include enough events to cause write amplification, though less than a full 48-hour replay. The team settled on a safety margin of 15 to 30 minutes beyond the observed maximum replica lag. They monitored the ts_ms of events at the consumer offset commit point and compared it to the wall clock. Over several weeks, they observed that 99.9% of events arrived within 10 minutes of their timestamp. The safety margin of 15 to 30 minutes was conservative but acceptable.
There is a philosophical tradeoff here: eventual consistency versus operational speed. The record-age filter is an explicit acknowledgment that some events may be lost in a rollback scenario. The team decided that the risk of missing a few late-arriving events was acceptable because the events represented state that would eventually be overwritten by newer events. For a read-model that is rebuilt from the source database periodically anyway, the gap was tolerable. If data loss were unacceptable, the full re-snapshot remained as a fallback, though it would be slower.
The team also considered a hybrid approach: filter by age first, then run a targeted re-snapshot of only the tables that had schema changes, using a custom snapshot query that selected rows modified after a certain timestamp. But that required database-level support for timestamp filtering, which not all tables had. The age filter was simpler and more portable.
Operational Lessons for Debezium Deployments
The incident taught several lessons that apply broadly to Debezium-based pipelines. First, default connector configuration assumes unlimited replay capacity. The standard advice to re-snapshot works only when the source database is small or the downstream systems can handle the write load. For any pipeline serving a real-time product, the assumption is dangerous.
Second, record-age awareness should be part of every incident runbook. The lead engineer now includes a section in every schema change ticket that estimates the replay time using the current event age distribution. If the estimate exceeds a threshold—say, 30 minutes—the team considers using the age filter or scheduling the change during low-traffic hours. This proactive planning has helped the team avoid unplanned downtime during schema changes, though the exact reduction varies by month and is not tracked as a single metric.
Third, compacted topics are not a substitute for age-based filtering. Compaction retains the latest value for each key, but it does not remove old keys. A topic that has been compacted for 48 hours still contains events for every key that was ever written, just with only the latest value. Replaying from a compacted topic still involves processing every key, which can be millions of events. The age filter reduces that set to only the keys that changed recently.
Finally, the pattern is not limited to Debezium. Similar CDC tools like Maxwell's Daemon and AWS DMS also emit events with timestamps. The same sidecar filter can be adapted for those pipelines, though the exact field name for the timestamp may differ. The lead engineer has shared the implementation internally as a reusable template, and it has been adopted by three other teams within the organization.
When Not to Use This Pattern
The record-age filter is not a universal solution. Event sourcing systems that rely on total event order for rebuilding state—such as those implementing CQRS with event-sourced aggregates—cannot drop any event without breaking the replay guarantee. For those systems, a full re-snapshot or a point-in-time recovery from a snapshot store is the only correct option.
Audit-compliance requirements may also forbid dropping any event. If the downstream system must prove that it processed every change event for regulatory purposes, filtering by age introduces a gap that auditors may not accept. In those cases, the team must either accept the full replay time or invest in a more sophisticated replay mechanism that replays all events but skips the processing of older ones.
High-latency database replicas are another contraindication. If the replicas routinely lag by 10 minutes or more, the safety margin needed to avoid data loss becomes large enough that the time savings shrink. In extreme cases, the margin might be so large that the filter replays almost as many events as the full topic. The team's monitoring of event age distribution is essential before adopting the pattern.
Small clusters where a full re-snapshot completes in under 15 minutes may not benefit from the added complexity. The filter introduces a new Kafka Streams application to manage, monitor, and deploy. For a team with a handful of tables and low event volume, the operational overhead may outweigh the time savings. The lead engineer recommends running a benchmark to compare full replay time against filtered replay time before standardizing on the pattern.
SaaS-managed CDC connectors, such as those provided by Confluent Cloud or Amazon MSK with Debezium, may not expose the consumer offset control needed to implement the sidecar filter. In those environments, the team is limited to the connector's built-in replay options, which typically mean re-snapshotting. The pattern applies only to self-managed Kafka clusters where the team controls consumer group offsets and can deploy custom stream processing.
Concrete Next Steps for Your Pipeline
If you are evaluating whether the record-age filter could work for your Debezium deployment, start by instrumenting your pipeline to measure event age distribution. Add a metric that captures the difference between the wall clock and the ts_ms field at the point where the consumer commits its offset. Run this for at least two weeks during normal operations to establish a baseline. If you observe that 99.9% of events are processed within, say, 15 minutes of their timestamp, then a cutoff of two hours (with a 15-minute safety margin) is likely safe.
Next, simulate a rollback scenario in a staging environment. Set up a Kafka Streams application with the age filter, configure the cutoff to two hours before the incident start time, and measure the time to drain the staging topic. Compare that to the time required for a full re-snapshot using the same data volume. If the filtered replay completes in under 30 minutes and the full snapshot takes several hours, the pattern is worth adopting.
Finally, document the cutoff selection criteria and the fallback procedure in your runbook. Include a step to verify that the filtered replay produced a consistent state by comparing a sample of cache entries against the source database. The lead engineer's team runs this verification as an automated test after every replay drill. The drill itself is scheduled quarterly, and the team has not needed to execute a full re-snapshot since the pattern was adopted over a year ago.