KubeMQ
Client SDKsC#How-to guidesQueues

Delayed Messages

Send KubeMQ queue messages with a delivery delay using the C# SDK so they arrive later.

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.

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

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

Code

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

  • 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.

Was this page helpful?

On this page