apache/kafka · error · TimeoutException
Timeout after waiting for {timeoutMillis} ms.
Error message
Timeout after waiting for {timeoutMillis} ms. What it means
Thrown by FutureRecordMetadata.get(long, TimeUnit) when the user-supplied wait elapses before the backing ProduceRequestResult completes. The future returned by KafkaProducer.send() wraps an in-flight batch; if the broker does not acknowledge that batch within the timeout the caller passes to get(), this TimeoutException is raised. It does NOT mean the record was lost — the batch may still be delivered later — only that the synchronous wait gave up. It surfaces user-side rather than from the producer's internal delivery timeout.
Source
Thrown at clients/src/main/java/org/apache/kafka/clients/producer/internals/FutureRecordMetadata.java:76
}
@Override
public RecordMetadata get() throws InterruptedException, ExecutionException {
this.result.await();
if (nextRecordMetadata != null)
return nextRecordMetadata.get();
return valueOrError();
}
@Override
public RecordMetadata get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
// Handle overflow.
long now = time.milliseconds();
long timeoutMillis = unit.toMillis(timeout);
long deadline = Long.MAX_VALUE - timeoutMillis < now ? Long.MAX_VALUE : now + timeoutMillis;
boolean occurred = this.result.await(timeout, unit);
if (!occurred)
throw new TimeoutException("Timeout after waiting for " + timeoutMillis + " ms.");
if (nextRecordMetadata != null)
return nextRecordMetadata.get(deadline - time.milliseconds(), TimeUnit.MILLISECONDS);
return valueOrError();
}
/**
* This method is used when we have to split a large batch in smaller ones. A chained metadata will allow the
* future that has already returned to the users to wait on the newly created split batches even after the
* old big batch has been deemed as done.
*/
void chain(FutureRecordMetadata futureRecordMetadata) {
if (nextRecordMetadata == null)
nextRecordMetadata = futureRecordMetadata;
else
nextRecordMetadata.chain(futureRecordMetadata);
}
RecordMetadata valueOrError() throws ExecutionException {View on GitHub (pinned to c31c9215e1)
Solutions
- Increase the timeout passed to future.get(...) so it comfortably exceeds delivery.timeout.ms plus linger.ms and retry backoff.
- Reduce linger.ms and retry.backoff.ms if lower latency is required and throughput can tolerate it.
- Use the callback (Producer.send(record, Callback)) instead of get() so delivery is asynchronous and this bounded wait never happens.
- Investigate broker-side latency (under-replicated partitions, slow disk, GC) if timeouts happen cluster-wide.
- Verify delivery.timeout.ms >= linger.ms + request.timeout.ms + retry.backoff window; an inconsistent set pushes records past any reasonable get() timeout.
Example fix
// before
RecordMetadata md = producer.send(rec).get(500, TimeUnit.MILLISECONDS);
// after
producer.send(rec, (metadata, e) -> {
if (e != null) { log.error("send failed", e); return; }
log.info("acked offset {}", metadata.offset());
}); Defensive patterns
Strategy: try-catch
Validate before calling
// Avoid blocking get() when not necessary; check done-ness first.
if (future.isDone()) {
RecordMetadata md = future.get();
} else {
// do not call future.get(timeout) unless you can act on a still-pending result
} Try / catch
try {
RecordMetadata md = future.get(remaining, TimeUnit.MILLISECONDS);
} catch (java.util.concurrent.TimeoutException te) {
// record is still in-flight, NOT failed; re-check later or rely on the Callback
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
} catch (java.util.concurrent.ExecutionException ee) {
// underlying produce failure is in ee.getCause()
} Prevention
- Prefer producer.send(record, callback) over blocking future.get(timeout); the broker notifies you on completion.
- Size get(timeout) against delivery.timeout.ms (send + retries + acks), not request.timeout.ms, to avoid spurious timeouts.
- Treat TimeoutException as 'still pending', not 'lost'; the record may still be delivered after you give up waiting.
When it happens
Trigger: Calling producer.send(record).get(N, TimeUnit.MILLISECONDS) (the bounded overload of Future.get) where N is shorter than the time needed for batching (linger.ms), retry backoff, or broker acknowledgement. Also hit when the application imposes a tight RPC deadline on each send while the broker is slow or partitions are under leader election.
Common situations: Tight per-request SLAs in request/reply services; brokers overloaded or in the middle of a leader election; linger.ms or retry.backoff.ms set higher than the get() timeout; network latency spikes; the record landed in a partition whose leader was momentarily unavailable and triggered a metadata refresh before sending.
Related errors
- Failed to update metadata after {timeoutMs} ms.
- Requested metadata update after close
- Timeout of {}ms expired before the position for partition {}
- Timeout of {}ms expired before the last committed offset for
- Failed to get offsets by times in {}ms
AI-assisted analysis of apache/kafka@c31c9215e1 (2026-08-03).
Data as JSON: /data/errors/20fd0fd16c6eacee.json.
Report an issue: GitHub.