An ack tells the broker what happened to a message. A completed ack moves the consumer group’s cursor past it, and that is all: the message stays in its partition until retention removes it.
Four answers
| Status | Spends a retry | What happens, and when to use it |
|---|---|---|
completed |
no | Done. The cursor moves past it. true means the same. |
failed |
yes | The lease is released and the message comes back after retryDelay. After the last retry it goes to the dead-letter queue. Use it for failures that may pass. false means the same. |
retry |
no | The lease is released and the message comes back at once. Use it to give work back (a worker that is shutting down). |
dlq |
no | Filed in the dead-letter queue now. Use it when trying again cannot help (a payload that will never parse). |
A failed or dlq ack can carry the reason (error: 'card declined' next to the group), and the dead-letter queue keeps it.
One message or a whole batch
queen.ack() takes one message or an array. With an array, one call acks the whole batch.
const messages = await queen.queue('events')
.group('auditor')
.subscriptionMode('all')
.batch(50)
.wait(true) // long poll: wait for messages, never return empty
.pop()
console.log('auditing', messages.length)
await queen.ack(messages, 'completed', { group: 'auditor' }) // the whole batch at oncemessages = await (
queen.queue("events")
.group("auditor")
.subscription_mode("all")
.batch(50)
.wait(True) # long poll: wait for messages, never return empty
.pop()
)
print("auditing", len(messages))
await queen.ack(messages, "completed", {"group": "auditor"}) # the whole batch at oncemessages, err := client.Queue("events").
Group("auditor").
SubscriptionMode(queen.SubscriptionModeAll).
Batch(50).
Wait(true). // long poll: wait for messages, never return empty
Pop(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println("auditing", len(messages))
_, err = client.Ack(ctx, messages, true,
queen.AckOptions{ConsumerGroup: "auditor"}) // the whole batch at once
if err != nil {
log.Fatal(err)
}let messages = queen
.queue("events")
.group("auditor")
.subscription_mode(SubscriptionMode::All)
.batch(50)
.wait(true) // long poll: wait for messages instead of returning empty
.pop()
.await?;
println!("auditing {}", messages.len());
queen.ack_all(&messages).await?; // the whole batch at once$messages = $queen->queue('events')
->group('auditor')
->subscriptionMode('all')
->batch(50)
->wait(true) // long poll: wait for messages, never return empty
->pop();
echo 'auditing ', count($messages), "\n";
$queen->ack($messages, 'completed', ['group' => 'auditor']); // the whole batch at oncejson messages = client.queue("events")
.group("auditor")
.subscription_mode("all")
.batch(50)
.wait(true) // long poll: wait for messages, never return empty
.pop();
std::cout << "auditing " << messages.size() << "\n";
client.ack(messages, true, {{"group", "auditor"}}); // the whole batch at onceEach message of an array can carry its own answer:
await queen.ack([
{ ...first, _status: 'completed' },
{ ...second, _status: 'failed', _error: 'timeout' },
], 'completed', { group: 'moderation' })await queen.ack([
{**first, "_status": "completed"},
{**second, "_status": "failed", "_error": "timeout"},
], "completed", {"group": "moderation"})// Go's Ack gives one status to all the messages it is passed: one call per status.
_, err = client.Ack(ctx, first, true, queen.AckOptions{ConsumerGroup: "moderation"})
if err != nil {
log.Fatal(err)
}
_, err = client.Ack(ctx, second, false,
queen.AckOptions{ConsumerGroup: "moderation", Error: "timeout"})
if err != nil {
log.Fatal(err)
}// One request, each message with its own answer: in the Rust client that is
// a transaction of acks.
queen
.transaction()
.ack_with(first, AckStatus::Completed)
.ack_with_reason(second, AckStatus::Failed, "timeout")
.commit()
.await?;$queen->ack([
[...$first, '_status' => 'completed'],
[...$second, '_status' => 'failed', '_error' => 'timeout'],
], 'completed', ['group' => 'moderation']);// QueenClient::ack gives a whole batch one answer, so a batch with an answer per
// message goes to the broker's batch ack route through the client's HTTP layer.
client.get_http_client()->post("/api/v1/ack/batch", {
{"consumerGroup", "moderation"},
{"acknowledgments", json::array({
{{"transactionId", first["transactionId"]}, {"leaseId", first["leaseId"]},
{"partitionId", first["partitionId"]}, {"status", "completed"}},
{{"transactionId", second["transactionId"]}, {"leaseId", second["leaseId"]},
{"partitionId", second["partitionId"]}, {"status", "failed"},
{"error", "timeout"}},
})},
});In Go, Rust and C++ one batch ack carries one status for all its messages, so their code sends one call per status, or a transaction of acks, where each message has its own.
In a batch, the cursor stops at the first message that will come back: a completed message after it is delivered again too.
Three things to know
- Inside
.each(), throw to fail a message (in JavaScript, Python and Go; see Pop and consume for the others). When the handler throws, the client nacks it and skips the rest of that partition’s batch. When the handler acksfaileditself and returns, the client does not know: it goes on with the partition’s next messages, their acks are refused (that part of the batch is coming back anyway), and they are handled twice. - Every ack answers HTTP 200, even a refused one. Read
successin the answer: a refused ack usually means the lease ran out and another worker has the message now. - An ack carries its consumer group and its lease. The client takes both from the message. Over HTTP you send them yourself: an ack without its lease is not checked, and can complete a batch another worker holds.
See also
- Retries and DLQ: where a
failedmessage goes. - Transactions: an ack that commits with the step’s other writes.
- Errors: the reason of a refused ack.