# Ack Range (/sdks/csharp/how-to/queues/ack-range)



## Overview [#overview]

A single poll response often bundles several messages into one batch, but "successfully processed" rarely applies to all of them uniformly — one handler might fail while its siblings succeed. Settling the whole batch together forces an all-or-nothing outcome: either you redeliver work you already finished, or you silently drop work you didn't. Per-message settlement lets each message's outcome reflect what actually happened to it, instead of the worst result in the batch.

Each message returned by `PollAsync` can be settled independently by calling `msg.AckAsync()` on it. Calling it on only some messages in the batch settles just those; the rest are left untouched — still pending, still redeliverable — until they're explicitly acked, nacked, or the visibility timeout expires.

**Gotchas:** messages you never touch aren't automatically fine — once the visibility timeout elapses, anything left unsettled goes back to the queue for redelivery, so a handler that forgets to call `AckAsync()` isn't "done," it's "will retry." Selective settlement only works when `AutoAck` is set to `false` on the poll request; with auto-ack on, the broker settles the entire batch the moment it's delivered, before your code runs. And there's no bulk "ack everything except these" call — you're responsible for tracking which messages you've already settled.

## Prerequisites [#prerequisites]

* KubeMQ server running on `localhost:50000`
* C# SDK installed (`dotnet add package KubeMQ.SDK.CSharp`)

## Code [#code]

```csharp title="Program.cs"
// KubeMQ .NET SDK — QueuesStream: Ack Range (Per-Message)
//
// This example demonstrates acknowledging specific messages individually
// using per-message AckAsync() via the downstream receiver API.
//
// Prerequisites:
//   - KubeMQ server running on localhost:50000
//   - dotnet run

using KubeMQ.Sdk.Client;
using KubeMQ.Sdk.Queues;
using System.Text;

await using var client = new KubeMQClient(new KubeMQClientOptions
{
    ClientId = "csharp-queues-ack-range-client",
});
await client.ConnectAsync();

Console.WriteLine("Connected to KubeMQ server");

for (var i = 1; i <= 5; i++)
{
    await client.SendQueueMessageAsync(new QueueMessage
    {
        Channel = "csharp-queues.ack-range",
        Body = Encoding.UTF8.GetBytes($"Message #{i}")
    });
}

Console.WriteLine("Sent 5 messages");

await using var receiver = await client.CreateQueueDownstreamReceiverAsync();

var batch = await receiver.PollAsync(new QueuePollRequest
{
    Channel = "csharp-queues.ack-range",
    MaxMessages = 5,
    WaitTimeoutSeconds = 10,
    AutoAck = false,
});

Console.WriteLine($"Received {batch.Messages.Count} messages");

foreach (var msg in batch.Messages)
{
    var body = Encoding.UTF8.GetString(msg.Body.Span);
    if (body.Contains("#1") || body.Contains("#3"))
    {
        await msg.AckAsync();
        Console.WriteLine($"Acked: {body}");
    }
    else
    {
        Console.WriteLine($"Skipped: {body} (stays in queue)");
    }
}

Console.WriteLine("Selective ack completed. Remaining messages stay in queue.");
Console.WriteLine("Done.");

```

## How It Works [#how-it-works]

* `PollAsync` with `AutoAck = false` puts each message into a "locked" state visible only to this receiver. Messages remain in the queue until explicitly settled within the visibility timeout.
* `msg.AckAsync()` removes the message permanently. Messages not acked (or nacked/requeued) are returned to the queue after the visibility timeout expires.
* This example selectively acks messages containing `#1` and `#3` and leaves the rest — those unacknowledged messages will reappear for the next consumer.
* Use this pattern for idempotent retry scenarios where only successfully processed items should be removed.

## Related [#related]

* [Pattern overview](/learn/queues/getting-started)
* [C# SDK Reference](/sdks/csharp/reference)
* [Send & Receive](/sdks/csharp/tutorials/send-receive)
* [Ack All](/sdks/csharp/how-to/queues/ack-all)
