Home / Data Platform / Streaming & CDC

Data PlatformTroubleshootingAdvanced

Kafka producer TimeoutException: expiring records - find whether it's the client, network or broker

Fix Kafka 'Expiring N records' errors by reading producer metrics to split time between client queue, network and broker, then tune delivery timeout, batching and in-flight settings without losing or duplicating data.

Deepak Bhardwaj5 min read

Applies toApache Kafka 2.x-4.x Java clients, librdkafka-based clients with equivalent settings, Confluent Platform, Amazon MSK

Symptom

Producers log something like this, and send callbacks fail:

org.apache.kafka.common.errors.TimeoutException: Expiring 42 record(s) for orders-3:
120000 ms has passed since batch creation

A close relative is:

Topic orders not present in metadata after 60000 ms.

These two have different causes. The first means a batch sat waiting too long for delivery. The second means the client couldn't get metadata, usually because of bootstrap, listener, authorization or topic-name problems, and it's bounded by max.block.ms. If you have the second one, read advertised listeners and authorization errors first.

The timing model

delivery.timeout.ms (default 120 seconds) limits the time from send() to the final result. It covers:

Stage Setting that affects it Metric to watch
Wait in the local buffer batch.size, linger.ms, buffer.memory record-queue-time-avg
Blocked on a full buffer max.block.ms waiting-threads, buffer-available-bytes
Each request to a broker request.timeout.ms (30 s) request-latency-avg
Retries and backoff retries, retry.backoff.ms record-retry-rate

Find which stage has used up the time before changing any of them. Raising delivery.timeout.ms alone only hides the problem and makes the backlog bigger.

Step 1: read the producer metrics

These come from the JMX group kafka.producer:type=producer-metrics,client-id=*:

Metric Meaning when it's high
record-queue-time-avg Batches wait in the client: too few connections in flight, or a slow broker
request-latency-avg The broker or network is slow
buffer-available-bytes near 0 The client produces faster than it can send
record-retry-rate Requests are failing and being retried
record-error-rate Records failed permanently
requests-in-flight Compare with the configured maximum
batch-size-avg Small batches mean many more requests

Print them from a Java client:

producer.metrics().forEach((name, metric) -> {
    if (name.group().equals("producer-metrics") &&
        List.of("record-queue-time-avg", "request-latency-avg", "buffer-available-bytes",
                "record-retry-rate", "record-error-rate", "batch-size-avg").contains(name.name())) {
        System.out.println(name.name() + " = " + metric.metricValue());
    }
});

Step 2: is it one partition or all of them?

kafka-topics.sh --bootstrap-server "$BOOTSTRAP" --describe --topic orders
kafka-topics.sh --bootstrap-server "$BOOTSTRAP" --describe --under-replicated-partitions

The partition in the error message tells you a lot. Compare errors across partitions and find their leaders:

Pattern Likely cause
All partitions led by one broker That broker is slow or sick
One partition only A hot key or leader problem
All partitions on all brokers The client, network or cluster-wide load
Only during a rolling restart Leader movement; check the retry and timeout budget

Step 3: check broker health

Key broker metrics:

Metric Concern
kafka.server:type=KafkaRequestHandlerPool,name=RequestHandlerAvgIdlePercent Low means I/O threads are saturated
kafka.network:type=SocketServer,name=NetworkProcessorAvgIdlePercent Low means network threads are saturated
kafka.network:type=RequestMetrics,name=TotalTimeMs,request=Produce Compare the mean with the 99th percentile
RemoteTimeMs,request=Produce High with acks=all: slow followers
kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions Above 0 means followers aren't keeping up

Look at the disks too:

iostat -xm 5 3
df -h /var/lib/kafka

A high RemoteTimeMs with acks=all means the leader is waiting on followers, which sends you looking at a slow disk or network on another broker. Also check under-replicated partitions.

Also check for quota throttling, which delays responses by design:

kafka-configs.sh --bootstrap-server "$BOOTSTRAP" --describe --entity-type clients

The client metric produce-throttle-time-avg shows whether it's being applied.

Step 4: baseline the path

Compare your app with a clean producer, using the same settings, from the same host:

kafka-producer-perf-test.sh --topic perf-test \
  --num-records 200000 --record-size 1024 --throughput -1 \
  --producer-props bootstrap.servers="$BOOTSTRAP" acks=all linger.ms=10 \
  batch.size=131072 compression.type=lz4

Use a dedicated test topic. If this is fast and your app isn't, look at the application: blocking calls in a callback, too many producers, or very large records.

Step 5: tune by cause

Finding Change
Queue time is high, batches are small Raise linger.ms (10-50) and batch.size, add compression
Buffer is exhausted Raise buffer.memory, or slow the source with backpressure
Broker is saturated Add partitions or brokers, and fix disks; don't just add retries
Large records Compress, and read message size limits
Failures in rolling restarts Keep delivery.timeout.ms long enough to cover leader changes

A reasonable starting point:

acks=all
enable.idempotence=true
compression.type=lz4
linger.ms=20
batch.size=131072
buffer.memory=134217728
request.timeout.ms=30000
delivery.timeout.ms=120000
max.block.ms=60000

The rule delivery.timeout.ms >= linger.ms + request.timeout.ms must hold, or the producer won't start. For more, see throughput tuning.

Handle the timeout correctly in the app

Warning

A TimeoutException doesn't prove the record failed. The broker may have written it after the client gave up. Blindly sending it again from your application can create a duplicate, because idempotence only protects retries inside the same producer session.

producer.send(record, (metadata, exception) -> {
    if (exception == null) return;
    if (exception instanceof RetriableException) {
        // Delivery state unknown: park it for a checked replay
        deadLetter.park(record, exception);
    } else {
        alert(record, exception);
    }
});
  • Always pass a callback. Without one, or without checking the returned Future, failures vanish.
  • Make consumers idempotent, using a business key, for records you replay.
  • Call producer.flush() and close() during shutdown, so buffered records aren't lost.

Caution

Don't set acks=1 or acks=0 to make the timeouts go away. It stops the errors by giving up the durability guarantee, and you can lose data on a broker failure.

Prevent it next time

  • Export producer metrics, and alert on record-error-rate above 0 and on rising queue time.
  • Load-test at expected peak traffic before launch, with a realistic record size.
  • Check RequestHandlerAvgIdlePercent during capacity reviews.
  • Related: min.insync.replicas errors and broker disk full.