Skip to main content
José David Baena

On this page

A Successful Kafka Send Is Not a Completed Business Operation

By José David Baena

Published on
/8 mins read

Your request times out after calling producer.send(). The callback never arrives. The incident channel asks one question: “Did we publish the order?”

The honest answer may be unknown. Kafka can append and replicate a batch while the response disappears. It can also lose the process before the batch leaves client memory. Those histories need different recovery actions even though the application sees the same missing result.

This reference uses Apache Kafka and the Java client 4.3.1. It separates five boundaries:

  1. the application accepts a business intent;
  2. the producer accepts a record into local memory;
  3. a partition leader appends it;
  4. the selected acknowledgement policy completes;
  5. the business effect finishes.

The Java producer configuration and KafkaProducer method contracts at tag 4.3.1 define the client-facing parts. The Kafka delivery design explains why delivery and processing guarantees stop at different places.

send() returning proves that the client accepted work, not that Kafka did

KafkaProducer.send() is asynchronous: after serialization, partition selection, metadata work, and buffer admission, it normally returns a future before the broker result exists. A process failure can erase a buffered record before any Produce request reaches Kafka.

That makes “the method returned” a weak publication boundary. If the application needs a recoverable acceptance point, it needs a durable intent ledger: for example, a database transaction that commits both the business change and an outbox row. The producer buffer is not that ledger.

The distinction changes what you measure:

ObservationWhat it establishesWhat remains open
Application committed an outbox rowA publication intent survives process restartKafka append, replication, and relay progress
send() returned a futureThe client accepted this call under its local rulesBroker receipt and final result
Callback/future completed successfullyThe selected acks contract completedExternal effect, all-media flush, and every consumer
Application wait expiredThe caller stopped waitingWhether the asynchronous send later completed
Business effect recordedThe chosen sink contract completedKafka progress and repeat protection

max.block.ms bounds specific blocking inside send(), including metadata and buffer allocation waits. It does not turn every serializer or partitioner call into the same clock. delivery.timeout.ms bounds reporting the send result after send() returns, including batching, request attempts, and acknowledgement waits. An application waiting 2 seconds on the returned future adds a fourth deadline; that wait expiring does not cancel protocol work.

Acknowledgement policies answer three different questions

An acknowledgement policy tells the producer which broker evidence must exist before a Produce request succeeds.

PolicySuccessful caller evidenceFailure boundary
acks=0No broker response is requiredReceipt and append remain unestablished
acks=1The leader appended locallyA permitted successor may lack the uncommitted tail
acks=allEvery current in-sync replica reached the required boundary in the stated profileMedia flush, correlated failure, and future election assumptions remain separate

The current in-sync replica set (ISR) matters more than replication factor alone. With replication factor three, min-ISR two, and ISR {A,B,C}, acks=all waits for A, B, and C in the stable case. If C has already left the committed ISR and the current ISR is {A,B}, A and B can satisfy the request. Min-ISR is an admission and visibility floor; it does not mean “pick any two replicas.”

Kafka 4.3.1 adds a detail that older shorthand often misses. The pinned Partition.maybeIncrementLeaderHW implementation does not advance the high watermark while the effective ISR is below min-ISR. An acks=1 append can therefore return leader-local success while normal consumers still cannot read the record below the replicated boundary. The replication article owns the deeper ISR, high-watermark, leader-epoch, and eligible-replica mechanism.

A missing response preserves uncertainty after possible transmission

Classify the failure by the strongest evidence the caller has:

Failure pointPublication state for this attemptRecovery implication
Serialization/configuration rejected before dispatchKnown not dispatchedFix or reject this attempt; another attempt still has its own history
Application wait expired after transmissionUNKNOWNKeep the in-flight result or reconcile before creating new protocol work
Broker returned a retriable error within delivery budgetProducer owns protocol retryDo not add an independent application resend by default
Final error after possible transmissionUNKNOWN unless another source resolves itPreserve operation identity and inspect durable evidence
Produce response was emitted but lostKafka may contain the appendRetry the same batch identity where the client contract permits

This is why catch (Exception) { newProducer.send(record); } is dangerous. Creating a new producer session and sending again changes the protocol identity. It may be the right recovery action, but only after the application decides how to recognize the original business intent.

Idempotence deduplicates a retried batch, not a repeated operation

Kafka producer idempotence uses a producer ID, producer epoch, partition, and per-record sequence. The broker can recognize the same retained batch and return its previous result without appending it again. The ProducerStateEntry.NUM_BATCHES_TO_RETAIN source shows a bounded five-batch metadata window per producer-partition.

That state does not hash payloads or understand an order ID. Three actions are not equivalent:

  1. the producer retries the same in-flight batch;
  2. the application calls send() again in the same session;
  3. a restarted application creates a new producer and resends.

The first can reuse the original sequence. The second allocates a new valid sequence. The third normally uses a new producer identity. Both later actions can append another record for one business operation.

Deterministic teaching model · apache-java-4.3.1

P07: Lost reply and identical idempotent retry

How can two transmissions produce one append?

Choose a Kafka teaching scenario
Change bounded local inputs

Changing these controls creates a custom local example. The share link keeps only the original curated preset; exports can include your bounded local values.

Fictional replicas A through E.

Teaching profile requires 1 ≤ min-ISR ≤ replication factor.

Producer waiting policy; it does not change consumer visibility.

The model displays the Java 4.3.1 resolved mode.

0–5 attempts beyond the first; not the Java default.

6 deliberately exposes an idempotence conflict.

Sequences advance by this record count.

Current outcome · step 0 of 13

No Kafka action has occurred; model state and caller evidence both begin empty.

curated preset

Default synthetic example loaded.

Producer

Resolved idempotence
enabled
The selected Java settings support idempotent production.
Session / next sequence
1 / p0:0 p1:0 p2:0
Sequences advance by record count independently per partition and session.
Current batch
none
Caller publication knowledge
UNKNOWN

Partition log

Leader / epoch
A / 1
LEO / HW
10 / 10
Exclusive next-offset boundaries for partition 0.
ISR / min-ISR / RF
ABC / 2 / 3
Log entries
0
0 append(s) currently name operation-a.

Observation

Response
none
Pre-append rejections
0
Routing map
v1
Process
running

Invariant results

  • HW stays at or below leader LEO

    holds in model

    The committed boundary never crosses the modeled append boundary.

  • Producer settings alone protect one business operation

    unknown

    This finite trace has at most one batch for operation-a, but producer identity is not a business-operation ledger.

  • Under-min-ISR state does not advance HW

    holds in model

    The reducer advances HW only when the declared ISR reaches min-ISR.

Ordered trace

The state table shows model truth. Each observer result says what the current caller can establish at that step.

Ordered deterministic actions through step 0. Print and exports contain the complete bounded trace.
StepActorActionObserver result
10 msproducerEnqueue event-aThe record is accepted into producer memory. No broker or durable application-intent evidence exists yet.
25 msproducerTransmit selected batchThe batch is in flight; transmission does not yet establish append or acknowledgement.
310 msbroker AAppend on leader AThe leader model appended the batch; producer completion still depends on the selected acknowledgement path.
420 msbroker BFollower B appends through leader LEOThe follower appends fetched bytes locally. The leader has not learned that new position yet.
525 msbroker BFollower B advertises its next offsetA later follower fetch advertises its next offset, so the leader can use that position as replication evidence.
636 mscontrollerFinalize stable ISR as A and BCommitted ISR membership becomes A, B for the next declared transition.
740 msleaderRecalculate the high watermarkHW advances to the minimum leader-known next offset across the current ISR: 11.
845 msbroker AEmit the Produce responseThe broker emits success after the current ISR reaches the appended end boundary.
950 msnetworkDrop the Produce responseThe model knows the broker emitted a result, but the producer/application did not observe it.
1060 msproducerRetry the identical batchThe producer retransmits the same session, epoch, and sequence range.
1165 msbroker ABroker checks the retried sequenceThe retained producer sequence identifies the same batch; the broker returns its prior append result without another log entry.
1270 msbroker AEmit retry responseThe broker emits success after the current ISR reaches the appended end boundary.
1375 msproducerCaller observes retry responseThe caller observes success under the selected acknowledgement policy. This does not establish a business effect.

Evidence and limits

Claims

Primary sources

Assumptions and known limits
  • Apache Kafka and Java client 4.3.1 with stable committed ISR membership between declared transitions.
  • Offsets are small exclusive boundaries; follower append and leader knowledge are separate actions.
  • No full ELR election, pending ISR expansion, reassignment, filesystem, or correlated-failure engine.
Default synthetic example loaded.

Run P07, then compare P08 and P09. The log-append count changes because the actions change protocol identity, not because the model reads a hard-coded “duplicate” label.

Effective Java settings belong in the publication contract

The Java 4.3 producer defaults to idempotence when no conflicting settings are supplied. It also defaults to acks=all, effectively unlimited configured retries, maximum in-flight five, and linger.ms=5. These are versioned Java defaults, not production recommendations.

Idempotence requires:

  • acks=all;
  • retries greater than zero;
  • max.in.flight.requests.per.connection no greater than five.

If you explicitly enable idempotence with incompatible values, the Java client raises ConfigException. If you omit the idempotence setting and provide a conflict, the client can resolve it to disabled. Record the resolved configuration, client family, client version, serializer, partitioner, timeout ownership, and transactional-ID strategy. A checkbox in an application config file is not enough.

Here is a publication-result shape, deliberately shown as contract pseudocode rather than a drop-in wrapper:

record PublicationResult(
    String operationId,
    String clientProfile,
    String resolvedAcks,
    boolean resolvedIdempotence,
    Status status,
    TopicPartition partition,
    Long offset) {}
 
enum Status {
    REJECTED_BEFORE_DISPATCH,
    ACKNOWLEDGED,
    UNKNOWN_AFTER_POSSIBLE_TRANSMISSION
}

operationId belongs to the application. Partition and offset belong to one log occurrence. Producer ID/epoch/sequence belong to one protocol session. Keep all three identity layers when recovery can cross a process restart.

What could go wrong

  • The application times out first and resends while the producer still retries. Two retry owners create new protocol work around an unresolved batch.
  • acks=all is read as “fsync every configured replica.” Kafka relies on page cache and replication; its persistence design does not promise per-request flush on every device.
  • A key routes differently after a partition or partitioner change. The per-partition sequence still works, but the business-order claim moved.
  • A transactional ID is treated as an operation ID. It fences writer incarnations; it does not suppress two valid committed transactions carrying the same operation.
  • A compatible broker inherits Apache semantics by name. Test the exact product, mode, and client. The portability audit keeps that evidence separate.

Write these seven lines before changing retry code

  1. Accepted intent: what durable state proves the application accepted the operation?
  2. Client profile: which library and exact version resolves the settings?
  3. Acknowledgement: what does callback success establish?
  4. Unknown result: which failures preserve UNKNOWN?
  5. Retry owner: producer, application, relay, or framework?
  6. Stable business identity: which key survives a new send and restart?
  7. Reconciliation: which durable source can resolve an ambiguous outcome?

The producer can tell you whether one protocol attempt met its contract. It cannot tell you whether your business operation is complete.


Sources and references

Share this post

HNPost to Hacker News

Follow future work

Follow public article updates through RSS. Intentionally unlisted posts stay out of the feed.

Working through a similar reliability boundary?

The Async Reliability Review turns one messaging or background-job flow into an evidence map, recovery plan, and owned next actions.

Keep reading