Delivery
Know when a record is safely handled: why the connector commits on acceptance rather than completion, what it does with each answer, and the three poison policies.
This is the durability contract of the Kafka connector: when an offset moves, and what happens to a record Duraton will not take. It is the page to read before deciding that a record was lost.
Commit on accept, not on completion
The connector commits a record's offset once Duraton has accepted the event, not when the workflow it started finishes.
poll record --> POST /events --> Duraton records the run --> 202 accepted --> COMMIT
|
the workflow runs on from here, for a second or an hour ---+
(the offset has already moved)That is deliberate and it is not a weaker guarantee. Acceptance is the durability boundary: the run exists and is recorded before the response is returned, so committing there never advances past non-durable work. Waiting for completion instead would mean a workflow containing a one-hour sleep or a human approval holds its partition for as long as that takes, and a partition held is lag for every other record behind it.
Delivery is at-least-once, and the default dedupe id - the topic:partition:offset triple - is stable
across any redelivery, which is what collapses at-least-once into effectively-once through Duraton's
own deduplication.
A record the destination will not take
| What Duraton answered | What the connector does |
|---|---|
| Accepted | Commit and continue. |
| Accepted, but the workspace is suspended | Do not commit. Stall with backoff, so the group's lag at the cluster becomes the visible signal instead of the events disappearing. |
401 or 403 | Halt with a configuration error. A rejected key rejects every record, so treating it as poison would empty a topic in minutes. |
429 | Back off and retry the same record. A rate limit is the one 4xx that waiting resolves. |
| Any other 4xx | Poison: apply the poison policy, commit, continue. |
| 5xx, or a transport fault | Back off and retry the same record, without committing. |
any other 4xx --> poison --> onPoison?
|
+-- commit-and-log (default) --> log, commit, continue
+-- commit-and-produce --------> write to poisonTopic, commit, continue
+-- halt ----------------------> stop consuming, exit non-zeroonPoison | Behaviour |
|---|---|
commit-and-log (default) | Log the topic, partition, offset, status and response body; commit; continue. |
halt | Stop consuming and exit non-zero, for a compliance posture that forbids skipping a record silently. |
commit-and-produce | Write the record to duraton.poisonTopic, then commit and continue. Requires that key, and that key is valid only under this policy. |
Under commit-and-produce the record is written with its original key, value and headers, plus
duraton.source.topic, duraton.source.partition, duraton.source.offset,
duraton.source.timestamp, duraton.error, and duraton.status when the destination answered at
all. The timestamp is there because the written record is a new record carrying its own: without it
a drain consumer cannot recover when the original was produced.
The write is acknowledged before the offset is committed, so a crash between the two redelivers the record rather than losing it. If the write cannot land the connector retries it on the same backoff as a refused delivery, indefinitely, and commits nothing: giving up would discard the record you explicitly asked to keep. Lag climbing on a poison topic that has stopped accepting writes is the intended signal.
Startup refuses a poison topic the connector is also consuming, whether it is named in
duraton.topics, keyed in duraton.assign, or matched by duraton.topicPattern. Listing it in
duraton.excludeTopics is the escape if a pattern is broad enough to catch it.
Backoff
A retried record is worked again on an exponential backoff. The window doubles from
duraton.retry.min and is capped at duraton.retry.max, and each wait is picked at random inside
that window, so a fleet that stalled on the same destination does not come back to it in step. A
wait is never shorter than duraton.retry.min: a destination that has just refused is the last
thing worth retrying immediately. That same window also governs the wait after a failed fetch
from the cluster, so it is one knob for how hard this process hammers something that is not
answering, whichever side is not answering.
| Bound | Value |
|---|---|
| First wait after a refusal | duraton.retry.min, 250ms unless you set it |
| Longest wait between attempts | duraton.retry.max, 30s unless you set it |
| One delivery attempt | 30 s at the transport, 45 s outer ceiling |
| How often a continuing stall repeats itself in the log | 30 s |
| Attempts before the connector gives up on a record | none: a stall ends when the destination changes its mind or the process is stopped |
Setting neither key keeps those two defaults. A min longer than max is refused at start-up with a
message naming both keys and both values, and so is a value of zero or less for either. That is a
start-up failure rather than a silent reordering, because an operator who inverted them meant
something by it.
There is no attempt limit, and deliberately no key for one. Giving up on a record would mean skipping work the destination never actually refused, which loses data nobody asked to lose - so a stall ends when the destination changes its mind, or when the connector is shut down, and never because a counter ran out. What tells you it is happening is the group's lag at the cluster, plus the stall warning in the log.
Next
- Observability - which series actually shows a stall.
- Filtering - dropping a record before it reaches any of this.
- Behaviour worth knowing - including why
duraton.assigncommits nothing. duraton:reference -onPoison,retry.min,retry.max.
Filtering
Drop records before they cost you a run: duraton.filter is a CEL expression evaluated before the event is sent, plus the two dialect traps that break it.
Publishing results back
Most Kafka integrations are terminal and should stay that way. When another consumer does need the outcome, publishing is a step in your own workflow with your own producer - no adapter, no config, no engine feature.