# Delayed Messages (/sdks/csharp/how-to/queues/delayed-messages)



<Callout type="info" title="Which to use">
  This is the task-oriented guide for sending delayed messages — send one with a delivery delay, confirm it's hidden, then receive it once the delay expires. For the `QueueMessage.DelaySeconds` property reference and its edge cases, see [Delay Policy](./delay-policy).
</Callout>

## Overview [#overview]

A **delivery delay** holds a queue message out of consumers' reach for a fixed window after it's sent — the message is accepted and persisted immediately, but invisible to pollers until the delay expires. It's the building block for scheduled work — a reminder to fire in an hour, a retry with back-off, a task queued for off-peak processing — without standing up a separate scheduler or cron service.

Set it with `DelaySeconds` on the `QueueMessage` before sending; the broker does the waiting. A poll via `PollAsync` against the channel before the delay elapses simply returns no messages — it isn't hidden in a separate place, it's the same queue, just not yet eligible for delivery. Once the delay window passes, the next poll retrieves it normally for acknowledgment.

**Gotchas:** the delay is set once at send time, per message, and can't be extended or shortened afterward — if you need a different wait, send a new message. A long delay still counts as an in-flight, persisted message, so it survives a broker restart, but it also occupies queue storage for the whole waiting period. Don't confuse this with a *visibility timeout* after delivery — that's a separate mechanism for redelivery on failed acknowledgment, not initial availability.

## 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 — Queues: Delayed Messages
//
// This example demonstrates sending messages with a delivery delay.
// The message becomes visible to consumers only after the delay expires.
// Uses QueueDownstreamReceiver.PollAsync for transactional message settlement.
//
// 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-delayed-messages-client",
});
await client.ConnectAsync();

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

// Send a message with 5-second delay
var sendResult = await client.SendQueueMessageAsync(new QueueMessage
{
    Channel = "csharp-queues.delayed-messages",
    Body = Encoding.UTF8.GetBytes("Delayed notification"),
    DelaySeconds = 5
});

Console.WriteLine($"Sent delayed message (5s delay): {sendResult.MessageId}");

// Create one receiver and reuse it for both polls
await using var receiver = await client.CreateQueueDownstreamReceiverAsync();

// Immediate receive — should not find the message yet
var immediateBatch = await receiver.PollAsync(new QueuePollRequest
{
    Channel = "csharp-queues.delayed-messages",
    MaxMessages = 1,
    WaitTimeoutSeconds = 1,
    AutoAck = false,
});

Console.WriteLine($"Immediate receive: {(immediateBatch.HasMessages ? "found" : "empty (expected)")}");

if (immediateBatch.HasMessages)
{
    await immediateBatch.AckAllAsync();
}

// Wait for delay to expire, then receive again
Console.WriteLine("Waiting 6 seconds for delay to expire...");
await Task.Delay(6000);

var delayedBatch = await receiver.PollAsync(new QueuePollRequest
{
    Channel = "csharp-queues.delayed-messages",
    MaxMessages = 1,
    WaitTimeoutSeconds = 5,
    AutoAck = false,
});

if (delayedBatch.HasMessages)
{
    foreach (var msg in delayedBatch.Messages)
    {
        Console.WriteLine($"Delayed receive: {Encoding.UTF8.GetString(msg.Body.Span)}");
        await msg.AckAsync();
    }
}

Console.WriteLine("Done.");

```

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

* `DelaySeconds = 5` sets a broker-side visibility delay. The message is accepted and persisted immediately but hidden from consumers until the delay expires.
* The immediate `PollAsync` with `WaitTimeoutSeconds = 1` returns empty — the message is not yet visible.
* After `Task.Delay(6000)` the delay window has passed, and the second `PollAsync` finds and delivers the message for manual acknowledgment.
* This pattern is useful for retry backoff (send with increasing delay on each failure) or scheduled job delivery without an external scheduler.

## Related [#related]

* [Delay Policy](./delay-policy) — field-level reference for `DelaySeconds`
* [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)
