How to Handle Message Acknowledgment (Ack/Nack) in Kourier

Kourier exposes RabbitMQ's AMQP 0-9-1 acknowledgment model through the AMQPChannel interface, allowing consumers to explicitly acknowledge (basicAck), negatively acknowledge (basicNack), or reject (basicReject) messages using delivery tags obtained from Delivery.message.

The guimauvedigital/kourier library provides Kotlin multiplatform support for AMQP brokers, implementing the full message acknowledgment lifecycle required for reliable message processing. Understanding how to properly acknowledge or reject deliveries is critical for building fault-tolerant consumers that prevent message loss while handling processing failures gracefully.

Understanding the AMQP Acknowledgment Model in Kourier

Kourier follows the RabbitMQ AMQP 0-9-1 specification where every message delivered to a consumer must be explicitly acknowledged. The library exposes this behavior through the AMQPChannel interface and its default implementation DefaultAMQPChannel.

When consuming messages, the noAck parameter in basicConsume determines the acknowledgment mode:

  • noAck = true: Automatic acknowledgment. The broker treats the message as acknowledged immediately upon delivery.
  • noAck = false: Manual acknowledgment. The client must call basicAck, basicNack, or basicReject using the delivery tag.

The interface definition in [AMQPChannel.kt lines 162-176] declares basicConsume with noAck: Boolean = false, defaulting to manual acknowledgment for safety.

Core Acknowledgment Methods

basicAck: Positive Acknowledgment

Use basicAck to signal successful message processing. The method signature in [AMQPChannel.kt lines 194-206] accepts:

  • deliveryTag: The broker-assigned identifier from Delivery.message.deliveryTag
  • multiple: When true, acknowledges all messages up to and including the supplied tag (batch acknowledgment)

The implementation in [DefaultAMQPChannel.kt lines 317-328] constructs a Frame.Method.Basic.Ack frame and writes it to the channel.

basicNack: Negative Acknowledgment with Options

Use basicNack when processing fails but you want control over requeue behavior. Declared in [AMQPChannel.kt lines 207-222], it accepts:

  • deliveryTag: The message identifier
  • multiple: Batch operation flag
  • requeue: true returns the message to the queue; false discards it (potentially to a dead-letter exchange)

The implementation in [DefaultAMQPChannel.kt lines 329-343] sends Frame.Method.Basic.Nack.

basicReject: Single Message Rejection

basicReject provides a convenience wrapper for rejecting a single message. Defined in **[AMQPChannel.kt lines 223-238], it is equivalent to basicNackwithmultiple = false`.

The implementation in [DefaultAMQPChannel.kt lines 362-380] handles the rejection logic.

basicRecover: Requeue Unacknowledged Messages

Use basicRecover after a consumer crash to requeue all unacknowledged messages. The method in [AMQPChannel.kt lines 239-250] accepts a requeue parameter:

  • requeue = true: Messages are requeued and may be redelivered to any consumer
  • requeue = false: Messages are redelivered to the original consumer

Implemented in **[DefaultAMQPChannel.kt lines 383-393]`.

Configuring Consumer Acknowledgment Behavior

Manual Acknowledgment Mode

For reliable processing, always consume with noAck = false (the default). This ensures messages remain in the broker until explicitly acknowledged:

val consumer = channel.basicConsume(
    queue = "task_queue",
    noAck = false,  // Manual acknowledgment required
    consumerTag = "worker-1"
)

This pattern prevents message loss during consumer failures but requires careful exception handling to avoid unacknowledged message buildup.

Automatic Acknowledgment Mode

Use noAck = true only for fire-and-forget scenarios where message loss is acceptable:

val consumer = channel.basicConsume(
    queue = "metrics_queue",
    noAck = true  // Auto-acknowledged by broker
)

In this mode, the broker removes the message from the queue immediately upon delivery, regardless of processing success or failure.

Practical Usage Patterns

Work Queue Pattern with Manual Ack

The standard work queue implementation demonstrates proper success and failure handling:

suspend fun processTasks(channel: AMQPChannel) {
    val consumer = channel.basicConsume("task_queue", noAck = false)
    
    for (delivery in consumer) {
        val body = delivery.message.body.decodeToString()
        try {
            // Process the task
            executeTask(body)
            // Success: acknowledge the message
            channel.basicAck(delivery.message, multiple = false)
        } catch (e: Exception) {
            // Failure: requeue for retry
            channel.basicNack(delivery.message, multiple = false, requeue = true)
        }
    }
}

See the complete example in [Work Queues tutorial lines 56-70].

Dead Lettering with Reject

When message content is invalid and should not be retried:

if (!validateMessage(payload)) {
    // Reject permanently (send to dead letter exchange)
    channel.basicReject(
        deliveryTag = delivery.message.deliveryTag,
        requeue = false
    )
    continue
}

This pattern routes invalid messages to a dead-letter queue for inspection rather than infinite retry loops.

Batch Acknowledgment for Performance

For high-throughput scenarios, accumulate delivery tags and acknowledge in batches:

var lastTag: ULong = 0u
var messageCount = 0

for (delivery in consumer) {
    processMessage(delivery)
    lastTag = delivery.message.deliveryTag
    messageCount++
    
    // Acknowledge every 10 messages
    if (messageCount % 10 == 0) {
        channel.basicAck(deliveryTag = lastTag, multiple = true)
    }
}

The multiple = true parameter in [DefaultAMQPChannel.kt lines 317-328] acknowledges all messages up to the specified tag, reducing network round-trips.

Handling Stale Delivery Tags with RobustAMQPChannel

When using connection recovery, delivery tags from old channels become invalid. The RobustAMQPChannel wrapper safely handles these:

val robustChannel = RobustAMQPChannel(wrapped = channel)

// After reconnection, old tags are silently ignored
robustChannel.basicAck(staleTag, multiple = false)  // No exception thrown

This implementation in [RobustAMQPChannel.kt lines 61-88] prevents PRECONDITION_FAILED errors during recovery by filtering out stale tags before they reach the underlying channel.

Summary

  • Manual acknowledgment (noAck = false) is the default and recommended mode for reliable message processing in Kourier.
  • Use basicAck to confirm successful processing, basicNack for failures with optional requeue, and basicReject as a convenience for single-message rejection.
  • The multiple parameter enables batch acknowledgment, improving throughput by reducing network overhead.
  • basicRecover requeues unacknowledged messages after consumer crashes, ensuring no message loss during failures.
  • Wrap channels with RobustAMQPChannel to safely handle stale delivery tags during connection recovery scenarios.

Frequently Asked Questions

What is the difference between basicNack and basicReject in Kourier?

basicReject is a convenience method that rejects a single message and is equivalent to calling basicNack with multiple = false. Both methods accept a requeue parameter that determines whether the message returns to the queue (true) or is discarded/dead-lettered (false). Use basicNack when you need to reject multiple messages at once using the multiple parameter, and basicReject for single-message rejection scenarios.

How do I prevent message loss when a Kourier consumer crashes?

Configure your consumer with noAck = false (manual acknowledgment) and ensure messages are only acknowledged after successful processing. If a crash occurs before acknowledgment, the broker retains the message and redelivers it to another consumer. Additionally, implement basicRecover(requeue = true) in your consumer startup logic to requeue any unacknowledged messages from previous sessions, ensuring they are not lost during consumer restarts.

Can I acknowledge multiple messages at once in Kourier?

Yes, use the multiple parameter set to true when calling basicAck or basicNack. This acknowledges all messages up to and including the specified deliveryTag in a single network round-trip. For example, channel.basicAck(deliveryTag = 50u, multiple = true) acknowledges every message with delivery tags 1 through 50. This batch acknowledgment pattern significantly improves throughput in high-volume consumer scenarios by reducing network overhead.

What happens if I try to acknowledge a message with a stale delivery tag?

In standard channels, attempting to acknowledge a message with a stale delivery tag (from a previous connection or channel) throws a PRECONDITION_FAILED error from the broker. To handle this safely during connection recovery, wrap your channel with RobustAMQPChannel, which intercepts acknowledgment calls and silently ignores those with stale delivery tags. This wrapper, implemented in RobustAMQPChannel.kt, prevents application crashes during network reconnection scenarios while maintaining message processing continuity.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →