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

  1. Increase the timeout passed to future.get(...) so it comfortably exceeds delivery.timeout.ms plus linger.ms and retry backoff.
  2. Reduce linger.ms and retry.backoff.ms if lower latency is required and throughput can tolerate it.
  3. Use the callback (Producer.send(record, Callback)) instead of get() so delivery is asynchronous and this bounded wait never happens.
  4. Investigate broker-side latency (under-replicated partitions, slow disk, GC) if timeouts happen cluster-wide.
  5. 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

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


AI-assisted analysis of apache/kafka@c31c9215e1 (2026-08-03). Data as JSON: /data/errors/20fd0fd16c6eacee.json. Report an issue: GitHub.