A Successful Kafka Send Is Not a Completed Business Operation
- 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:
- the application accepts a business intent;
- the producer accepts a record into local memory;
- a partition leader appends it;
- the selected acknowledgement policy completes;
- 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:
| Observation | What it establishes | What remains open |
|---|---|---|
| Application committed an outbox row | A publication intent survives process restart | Kafka append, replication, and relay progress |
send() returned a future | The client accepted this call under its local rules | Broker receipt and final result |
| Callback/future completed successfully | The selected acks contract completed | External effect, all-media flush, and every consumer |
| Application wait expired | The caller stopped waiting | Whether the asynchronous send later completed |
| Business effect recorded | The chosen sink contract completed | Kafka 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.
| Policy | Successful caller evidence | Failure boundary |
|---|---|---|
acks=0 | No broker response is required | Receipt and append remain unestablished |
acks=1 | The leader appended locally | A permitted successor may lack the uncommitted tail |
acks=all | Every current in-sync replica reached the required boundary in the stated profile | Media 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 point | Publication state for this attempt | Recovery implication |
|---|---|---|
| Serialization/configuration rejected before dispatch | Known not dispatched | Fix or reject this attempt; another attempt still has its own history |
| Application wait expired after transmission | UNKNOWN | Keep the in-flight result or reconcile before creating new protocol work |
| Broker returned a retriable error within delivery budget | Producer owns protocol retry | Do not add an independent application resend by default |
| Final error after possible transmission | UNKNOWN unless another source resolves it | Preserve operation identity and inspect durable evidence |
| Produce response was emitted but lost | Kafka may contain the append | Retry 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:
- the producer retries the same in-flight batch;
- the application calls
send()again in the same session; - 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?
Current outcome · step 0 of 13
No Kafka action has occurred; model state and caller evidence both begin empty.
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.
| Step | Actor | Action | Observer result |
|---|---|---|---|
| 10 ms | producer | Enqueue event-a | The record is accepted into producer memory. No broker or durable application-intent evidence exists yet. |
| 25 ms | producer | Transmit selected batch | The batch is in flight; transmission does not yet establish append or acknowledgement. |
| 310 ms | broker A | Append on leader A | The leader model appended the batch; producer completion still depends on the selected acknowledgement path. |
| 420 ms | broker B | Follower B appends through leader LEO | The follower appends fetched bytes locally. The leader has not learned that new position yet. |
| 525 ms | broker B | Follower B advertises its next offset | A later follower fetch advertises its next offset, so the leader can use that position as replication evidence. |
| 636 ms | controller | Finalize stable ISR as A and B | Committed ISR membership becomes A, B for the next declared transition. |
| 740 ms | leader | Recalculate the high watermark | HW advances to the minimum leader-known next offset across the current ISR: 11. |
| 845 ms | broker A | Emit the Produce response | The broker emits success after the current ISR reaches the appended end boundary. |
| 950 ms | network | Drop the Produce response | The model knows the broker emitted a result, but the producer/application did not observe it. |
| 1060 ms | producer | Retry the identical batch | The producer retransmits the same session, epoch, and sequence range. |
| 1165 ms | broker A | Broker checks the retried sequence | The retained producer sequence identifies the same batch; the broker returns its prior append result without another log entry. |
| 1270 ms | broker A | Emit retry response | The broker emits success after the current ISR reaches the appended end boundary. |
| 1375 ms | producer | Caller observes retry response | The caller observes success under the selected acknowledgement policy. This does not establish a business effect. |
Evidence and limits
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.
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.connectionno 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=allis 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
- Accepted intent: what durable state proves the application accepted the operation?
- Client profile: which library and exact version resolves the settings?
- Acknowledgement: what does callback success establish?
- Unknown result: which failures preserve
UNKNOWN? - Retry owner: producer, application, relay, or framework?
- Stable business identity: which key survives a new send and restart?
- 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
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


