I spent a few weeks last year getting a Debezium Oracle CDC pipeline to a point where it felt stable enough to trust in production. Not just "events are flowing" stable, but "nobody gets paged at 2am" stable.
Along the way I made most of the obvious mistakes. I tuned the wrong things first. I optimized for throughput when the use case actually needed low latency. I assumed the Oracle side was the bottleneck when the real problem was the sink.
This post is what I would have wanted to read before I started.
First, Decide What You Are Actually Optimizing For
Before changing any configuration, you need to answer one question: throughput or latency?
They pull in opposite directions, and the settings that help one hurt the other. If you skip this step, you end up with a pipeline that is mediocre at both.
Throughput means moving a lot of data efficiently. This is what you need when catching up from lag, doing initial loads, or replicating high-volume tables. You want larger batches, fewer round trips, and a pipeline that works hard in big chunks.
Latency means changes appearing downstream quickly. This is what matters for near-real-time feeds, operational dashboards, or anything where a user is waiting. You want smaller batches, faster polling, and a pipeline that never sits idle waiting for a batch to fill.
You cannot have both at maximum. Pick one, then tune toward it.
The Two Settings That Matter Most
LogMiner Batch Size
log.mining.batch.size.default controls how many redo log entries Debezium targets per mining session. Starting and stopping LogMiner is expensive. The more you read per session, the less time you waste on setup and teardown.
For high throughput, push this well above the default (around 20,000). I used 100,000 in our setup and the difference was significant. There is also a log.mining.batch.size.max that the connector uses automatically when it detects growing lag. Think of it as a catch-up mode that activates when the pipeline falls behind.
For low latency, keep it smaller. You pay more overhead per event, but data flows faster.
Sink Concurrency
This one surprised me. I had assumed Oracle was the main bottleneck. It was not.
We were using an HTTP sink with a limit of 100 messages per batch. If the sink processes batches sequentially - send 100 events, wait for a 200 OK, send the next 100 - you are limited by HTTP round-trip latency multiplied by the number of batches. At 50ms per request, you are moving at most 2,000 events per second regardless of what LogMiner can produce.
The fix was to send batches in parallel. If you are using a Kafka-based sink, this maps directly to consumer parallelism and partition count. For HTTP sinks, quarkus.rest-client.connection-pool-size matters a lot more than most tuning guides suggest. Increasing it and making sure the sink was processing concurrently improved throughput more than any Oracle-side setting I changed.
If you are bottlenecked and the Oracle metrics look healthy, check the sink first.
Constrained Environments
Not every deployment has generous resources. A few things helped when we needed to run lean:
Low memory: The internal event queue (max.queue.size) is the biggest heap consumer. Reducing it limits throughput but keeps memory usage predictable. Pair it with a smaller fetch size from LogMiner and you can run in a surprisingly small container without hitting OOM.
Low CPU: Adding a sleep between LogMiner queries (log.mining.sleep.time.default.ms) is the most effective single change for reducing database load. It feels counterintuitive but it works. You pay in latency, and if the use case can accept that, it is a good trade. Also worth doing: disable schema serialization by setting key.converter.schemas.enable=false and value.converter.schemas.enable=false. The schema payload is verbose and serializing it on every event adds up faster than you expect.
Look at the Metrics Before Touching Anything
My mistake early on was jumping into configuration changes before I had a baseline. I was flying blind and creating new problems while trying to fix the original one.
Three JMX metrics tell you almost everything:
LagFromSource- how far behind the pipeline isBatchSize- whether you are consistently hitting the ceilingScnGapCount- whether LogMiner is losing its place in the redo logs
If BatchSize is always at the maximum, the source is working hard but the sink cannot consume fast enough. If LagFromSource is growing during peak hours but recovers overnight, throughput is the problem. If ScnGapCount is climbing, something is wrong with the LogMiner session itself.
Get these into Prometheus via the JMX exporter before changing anything. Two hours of instrumentation will save you days of guessing.
One More Thing About LogMiner and Schemas
If you are using the online_catalog mining strategy (which is faster and simpler than the alternative), be careful about DDL changes on captured tables while the connector is stopped or lagging. The connector uses the live database dictionary to resolve object IDs to table names. If the schema changes while Debezium is behind, the historical log entries and the current dictionary can get out of sync, and recovery usually means a full re-snapshot.
This is not a reason to avoid online_catalog. It is just worth knowing before you do a schema migration at 11pm.
An Interactive Tool for This
I put together a tool that maps these tuning profiles to actual configuration values. You pick a scenario (high throughput, low latency, low CPU, or low memory) and it generates an application.properties file with the recommended settings and the reasoning behind each one.
It is at changedatacapture.net/debezium-oracle-performance-tuning/. There is also a full technical guide if you want to understand the mechanics rather than just copy the settings.
The settings are a starting point, not a formula. What works depends on your Oracle environment, your sink throughput, your network, and what the use case actually needs. But having a principled starting point and knowing which levers to pull is most of the work.