Ack semantics
Esta página aún no está disponible en tu idioma.
The ack is the core primitive of reliable delivery — the producer releases buffered messages once acked. The controller manages ack timing for you; how the handler resolves controls whether the ack fires.
How the ack actually fires
Section titled “How the ack actually fires”1. ConsumerController receives a Delivery envelope2. Controller checks per-producer dedup state → known seq → re-ack only, skip handler → new seq → call handler(body), wait for resolution3. Handler resolves → controller sends Ack to msg.replyTo Handler throws → controller does NOT ack4. Producer receives Ack → removes seq from its in-flight buffer → frees a window slot for the next sendThe handler is the only user-facing knob. Whatever happens
inside handler(body) is your code; whatever happens
around it (dedup, ack, replyTo wiring, retransmit timing)
is the controller’s job.
Three patterns
Section titled “Three patterns”| Pattern | Handler shape | Guarantee |
|---|---|---|
| Resolve on success (default) | Handler does the work, returns void or resolved Promise | At-least-once with in-memory dedup |
| Throw on failure | Handler throws / rejects when processing failed → controller skips ack → producer resends | At-least-once with retry |
| Persist before resolve | Handler awaits a journal write before returning | Effectively-once across consumer restart |
new ConsumerController<T>({ handler: async (body) => { await this.processIdempotently(body); // ack-on-resolve },});What happens between processing and ack
Section titled “What happens between processing and ack”1. Handler starts processing (body)2. Handler completes (resolves)3. Controller serialises an Ack envelope4. Controller tells the producer's replyTo ref5. Producer removes msg from unacked bufferIf the consumer crashes between 2 and 4, the ack never
reaches the producer. Producer retransmits after
resendTimeoutMs. The controller’s in-memory dedup is gone
after the crash, so the same seq runs the handler again — make
it idempotent, or persist processed-seq alongside business
state.
Persist before resolve
Section titled “Persist before resolve”For effectively-once processing of side effects, await the
journal write inside the handler. When handler(body)
resolves, both your event AND the implicit “processed this
seq” marker are durable; the controller’s subsequent Ack just
tells the producer it can free its slot.
class PersistentEventLog extends PersistentActor<Cmd, Event, State> { // …onCommand / onEvent omitted…}
const log = system.spawnAnonymous(Props.create(() => new PersistentEventLog()));
const consumer = system.spawn( Props.create(() => new ConsumerController<OrderEvent>({ handler: async (order) => { // ask() resolves once the PersistentActor has journalled // the event. If the consumer crashes before this resolves, // the producer will retransmit and the handler runs again — // but the PersistentActor's own dedup (event-id or seq in // event payload) handles the rerun. await log.ask({ kind: 'append', order }, 30_000); }, })),);The sequence on a clean run:
- Delivery arrives.
- Controller calls
handler(body). - Handler asks the journal, the journal writes, the ask resolves.
- Controller sends Ack → producer frees its slot.
On crash between step 2’s start and step 3’s resolve:
- Before the journal write commits: producer retransmits; the handler runs again; the journal-side dedup catches it. Net: zero duplication.
- After the journal write commits, before the controller’s Ack reaches the producer: producer retransmits; the handler runs again; the journal-side dedup short-circuits and the controller re-acks immediately. Net: zero duplication.
For end-to-end durability, pair this with persistent producer state — store the producer’s in-flight buffer in a journal so a producer-side crash doesn’t lose unacked messages.
Crash scenarios
Section titled “Crash scenarios”Producer crash before send
Section titled “Producer crash before send”Producer sends msg #5 → crash before transmit completesRecovery: producer restarts. Without persistence, msg #5 is lost. With persistence, the message is in the producer’s unacked-buffer journal; transmits on recovery.
Consumer crash before processing
Section titled “Consumer crash before processing”Consumer receives msg → crashes before handler runsNo ack sent; producer retransmits. Idempotent handler dedupes (or processes for the first time).
Network drop between transmission and ack
Section titled “Network drop between transmission and ack”Producer sends msg #5Consumer processes msg #5Consumer sends ack #5Network drops the ack envelopeProducer retransmits msg #5 after timeout. Consumer dedupes (highWatermark already at 5); re-acks.
The dedup + re-ack pattern means transient acks are recoverable — eventually one arrives.
Slow consumer
Section titled “Slow consumer”Producer.windowSize = 16Consumer takes 10s per messageBackpressure: after 16 unacked messages are in flight, the
producer stops sending further messages until an Ack frees
a slot. Incoming { kind: 'reliable-delivery.send' } tells
queue inside the producer until the window opens.
This is good — the system can’t get further ahead of itself than the flow-control window.
Cluster failover
Section titled “Cluster failover”Consumer on node-A processes msg #5; controller queues the AckNode-A crashes before the Ack tell completesProducer doesn’t see Ack within resendTimeoutMs → retransmits.
If the consumer is sharded (the entity moves during failover), the new instance:
- Receives the retransmit.
- Has empty in-memory dedup state (fresh
ConsumerController). - Calls the handler — which must persist its own processed-seq for effectively-once.
For sharded consumers, always pair the handler with persistence — otherwise the controller’s dedup resets on every rebalance and you get full reprocessing.
Ack as a guarantee (or not)
Section titled “Ack as a guarantee (or not)”The Ack is fire-and-forget from the controller to the
producer’s replyTo. The controller doesn’t await its
delivery — if the network drops the Ack, the producer
retransmits after resendTimeoutMs and the controller’s
dedup re-acks immediately.
This means: your handler doesn’t pay any latency cost for
the Ack. The controller emits the Ack after handler(body)
resolves and moves on.
When NOT to use the controllers
Section titled “When NOT to use the controllers”Where to next
Section titled “Where to next”- Delivery overview — the bigger picture.
- ProducerController — the sender side.
- ConsumerController — the receiver side.
- PersistentActor — for end-to-end durable processing.