KubeMQ
IntegrationsMassTransitHow-to guides

Queues (Send)

Map MassTransit Send to KubeMQ Queues for durable point-to-point delivery — delayed send, TTL, batch consumers, priority queues, and competing consumers.

Overview

MassTransit Send maps to KubeMQ Queues — durable, point-to-point messaging. Where Publish fans a message out to every active subscriber, Send directs a message to a single endpoint where exactly one consumer receives it. Messages are persisted in the queue until consumed, so the consumer need not be running at the moment you send: the broker holds the message and delivers it when a consumer is available.

→ For what a Queue is and KubeMQ's queue semantics, see Queues. This page documents the MassTransit transport's API surface only.

Delivery is acknowledgment-based. After a consumer's Consume method completes successfully, the transport acks the message and KubeMQ removes it from the queue. If the consumer throws, the transport nacks the message, and faulted messages route to the {queue}_error channel (skipped messages route to {queue}_skipped).

A sent message lands in a durable KubeMQ Queue and is delivered to exactly one consumer; faults route to the _error channel.

Send is the right choice when each message must be processed once and must survive a consumer being offline. For fan-out delivery to all subscribers, use Events instead.

API surface

MemberWherePurpose
bus.GetSendEndpoint(Uri) / IKubeMQRider.GetSendEndpoint(Uri, ct)bus / riderResolve a send endpoint for a queue: or kubemq://host:port/channel address
ISendEndpoint.Send<T>(message, ctx => …)send endpointSend a message; the context pipe sets Delay, TimeToLive, and headers
SendContext.Delaysend contextDelayed delivery → QueueMessage.DelaySeconds
SendContext.TimeToLivesend contextPer-message TTL → QueueMessage.ExpirationSeconds
IKubeMQEndpointTransportConfigurator.ExpirationSecondsConfigureKubeMQPer-endpoint TTL for every message the endpoint produces
IKubeMQBusFactoryConfigurator.UsePriorityQueues(…)bus factoryEnable weighted _high/_normal/_low priority channels
IKubeMQReceiveEndpointConfigurator.PollTimeoutSeconds / .MaxPollMessagesreceive endpointTune long-poll wait and batch size per endpoint

Send endpoints use the kubemq://host:port/channel-name URI scheme. The short queue:channel-name form resolves to the same queue channel on the configured host. Full naming and address rules live in the reference.

Usage

Basic send and receive

To send to a queue, obtain a send endpoint for the queue: address and call Send. The matching consumer implements IConsumer<T>. Unlike Events, a queue endpoint does not call UseVolatileEvents() — the default endpoint behavior is a KubeMQ Queue consumer.

Program.cs
using MassTransit;
using MassTransit.KubeMQTransport;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMassTransit(x =>
{
    x.AddConsumer<OrderConsumer>();
    x.UsingKubeMQ((context, cfg) =>
    {
        cfg.Host("localhost", 50000);
        cfg.ReceiveEndpoint("send-basic-queue", e =>
        {
            e.ConfigureKubeMQ(t => { });
        });
    });
});

builder.Services.AddHostedService<BasicSendSender>();

var host = builder.Build();
await host.RunAsync();

// --- Message type ---
public record OrderMessage(Guid OrderId, string ProductName, decimal Amount);

// --- Consumer ---
public class OrderConsumer(ILogger<OrderConsumer> logger) : IConsumer<OrderMessage>
{
    public Task Consume(ConsumeContext<OrderMessage> context)
    {
        logger.LogInformation(
            "Received order: OrderId={OrderId}, Product={Product}, Amount={Amount:C}",
            context.Message.OrderId,
            context.Message.ProductName,
            context.Message.Amount);
        return Task.CompletedTask;
    }
}

// --- Background sender ---
public class BasicSendSender(
    ILogger<BasicSendSender> logger,
    IHostApplicationLifetime lifetime) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await Task.Delay(2000, stoppingToken);

        var rider = KubeMQRiderAccessor.Current
            ?? throw new InvalidOperationException("KubeMQ rider not started.");

        var endpoint = await rider.GetSendEndpoint(
            new Uri("kubemq://localhost:50000/send-basic-queue"), stoppingToken);

        var order = new OrderMessage(Guid.NewGuid(), "Widget", 29.99m);
        await endpoint.Send(order, stoppingToken);
        logger.LogInformation("Message sent successfully.");

        await Task.Delay(5000, stoppingToken);
        lifetime.StopApplication();
    }
}

The short queue:channel-name form (as in bus.GetSendEndpoint(new Uri("queue:order-processing"))) resolves to the same queue channel as the fully-qualified kubemq://host:port/channel-name. Inside a hosted service, KubeMQRiderAccessor.Current gives direct access to the rider's GetSendEndpoint.

Delayed delivery

Set ctx.Delay on the send context to schedule a message for later. The transport maps Delay to QueueMessage.DelaySeconds: the message is enqueued immediately but stays invisible to consumers until the delay elapses.

Delayed send (maps to QueueMessage.DelaySeconds)
var endpoint = await rider.GetSendEndpoint(
    new Uri("kubemq://localhost:50000/send-delayed-queue"), stoppingToken);

var reminder = new ReminderMessage(
    Guid.NewGuid(), "Follow up with client", DateTimeOffset.UtcNow.AddSeconds(5));

// Set the delay on the send context — transport maps this to QueueMessage.DelaySeconds
await endpoint.Send(reminder, context =>
{
    context.Delay = TimeSpan.FromSeconds(5);
}, stoppingToken);

Delayed delivery is a queue-only feature. It is not supported for Publish (Events or EventsStore) — attempting to delay a published event throws KubeMQTransportConfigurationException. Use Send whenever you need scheduled delivery.

Message expiration (TTL)

A queue message can carry a time-to-live. If it is not consumed within the TTL, KubeMQ discards it rather than delivering a stale message. Set it per message with ctx.TimeToLive, or per endpoint with ExpirationSeconds in ConfigureKubeMQ. Both map to QueueMessage.ExpirationSeconds.

Per-message TTL
var endpoint = await rider.GetSendEndpoint(
    new Uri("kubemq://localhost:50000/send-expiration-queue"), stoppingToken);

var coupon = new CouponMessage(Guid.NewGuid(), "SAVE20", 20.0m);

await endpoint.Send(coupon, context =>
{
    context.TimeToLive = TimeSpan.FromSeconds(30);
}, stoppingToken);
Per-endpoint expiration
cfg.ReceiveEndpoint("expiration-demo-queue", e =>
{
    // Messages not consumed within 1 hour are discarded by KubeMQ
    e.ConfigureKubeMQ(k =>
    {
        k.ExpirationSeconds = 3600;
    });
});

An expired message is dropped, not moved to {queue}_error — TTL is distinct from dead-letter routing.

Custom headers

Custom headers set on the send context travel with the message and are readable on the consumer. The transport maps each user header to a KubeMQ tag with the MT-Header-{name} prefix (so tenant-id becomes MT-Header-tenant-id); MassTransit envelope headers map to MT-* tags. The full mapping table lives in the reference.

Setting and reading custom headers
// Send
await endpoint.Send(invoice, context =>
{
    context.Headers.Set("tenant-id", "acme-corp");
    context.Headers.Set("x-correlation-hint", "batch-2024-q1");
}, stoppingToken);

// Receive
public class HeaderConsumer(ILogger<HeaderConsumer> logger) : IConsumer<InvoiceMessage>
{
    public Task Consume(ConsumeContext<InvoiceMessage> context)
    {
        var tenantId = context.Headers.Get<string>("tenant-id") ?? "(not set)";
        logger.LogInformation("Received invoice; tenant-id={TenantId}", tenantId);
        return Task.CompletedTask;
    }
}

You set and read plain MassTransit header names; the transport handles the MT-Header- tag prefix on the wire.

Batch consuming

A consumer can receive multiple messages per Consume call by implementing IConsumer<Batch<T>>. Configure batching on the consumer with BatchOptionsSetMessageLimit caps how many messages accumulate before delivery, SetTimeLimit flushes a partial batch after a timeout. The number the transport pulls per poll is governed by the endpoint's MaxPollMessages (default 32).

Batch consumer
public record BatchOrderMessage(Guid OrderId, string ProductName, decimal Amount, int Sequence);

public class OrderBatchConsumer(ILogger<OrderBatchConsumer> logger)
    : IConsumer<Batch<BatchOrderMessage>>
{
    public Task Consume(ConsumeContext<Batch<BatchOrderMessage>> context)
    {
        logger.LogInformation("Received batch of {Count} messages", context.Message.Length);

        foreach (var msg in context.Message)
        {
            logger.LogInformation(
                "  Batch item: Seq={Sequence}, OrderId={OrderId}",
                msg.Message.Sequence, msg.Message.OrderId);
        }

        return Task.CompletedTask;
    }
}

// Configures batch size and time limits
public class OrderBatchConsumerDefinition : ConsumerDefinition<OrderBatchConsumer>
{
    protected override void ConfigureConsumer(
        IReceiveEndpointConfigurator endpointConfigurator,
        IConsumerConfigurator<OrderBatchConsumer> consumerConfigurator,
        IRegistrationContext context)
    {
        consumerConfigurator.Options<BatchOptions>(options => options
            .SetMessageLimit(10)
            .SetTimeLimit(TimeSpan.FromSeconds(5)));
    }
}

MaxPollMessages controls how many messages the transport fetches from KubeMQ per poll; BatchOptions.SetMessageLimit controls how many MassTransit delivers to one Consume call. Tune MaxPollMessages to be at least as large as your batch message limit so a full batch can be assembled from a single poll.

Competing consumers

Multiple receive endpoints that share the same channel name form a consumer group. KubeMQ load-balances queue messages across the group, so each message is delivered to exactly one consumer — the competing-consumers pattern, and how you scale queue throughput horizontally.

Competing consumers in one process
x.UsingKubeMQ((context, cfg) =>
{
    cfg.Host("localhost", 50000);

    // Two endpoints on the same channel compete for messages.
    // KubeMQ delivers each message to exactly one member.
    cfg.ReceiveEndpoint("competing-consumers-queue", e =>
    {
        e.ConfigureKubeMQ(t => { });
    });

    cfg.ReceiveEndpoint("competing-consumers-queue", e =>
    {
        e.ConfigureKubeMQ(t => { });
    });
});

The same principle scales across processes: deploy multiple instances that each declare a receive endpoint with the same channel name, and KubeMQ distributes the queue's messages across them — at-least-once delivery with automatic load balancing, no partitioning or sharding configuration required.

Priority queues

Priority queues let high-priority work jump ahead of lower-priority work. Enable them with cfg.UsePriorityQueues(). The transport creates three channels per endpoint — {queue}_high, {queue}_normal, {queue}_low — and polls them with weighted round-robin (default weights High=3, Normal=2, Low=1). You send to a level by addressing the suffixed channel.

Priority queues
x.UsingKubeMQ((context, cfg) =>
{
    cfg.Host("localhost", 50000);

    // Default weights: high=3, normal=2, low=1
    cfg.UsePriorityQueues();

    cfg.ReceiveEndpoint("pq-basic-orders", e =>
    {
        e.ConfigureKubeMQ(t => { });
    });
});

// Send to a specific priority level by addressing the suffixed channel
var highEndpoint = await rider.GetSendEndpoint(
    new Uri("kubemq://localhost:50000/pq-basic-orders_high"), stoppingToken);
await highEndpoint.Send(new PriorityOrder(Guid.NewGuid(), "HIGH", "HighItem", 100.00m), stoppingToken);

To change the polling ratios, pass a configurator and call SetWeights(highWeight, normalWeight, lowWeight) on IPriorityQueueConfigurator:

Custom priority weights (5:3:1)
// high-priority channel is polled 5x per cycle vs 1x for low
cfg.UsePriorityQueues(p => p.SetWeights(highWeight: 5, normalWeight: 3, lowWeight: 1));

Priority is enforced by weighted polling, not strict ordering. A higher weight means a channel is sampled more frequently, so high-priority messages are processed first under load — but low-priority messages still make progress and are never starved.

Tuning poll behavior

Queue receive uses long-polling. Two settings control it, set globally on KubeMQTransportOptions or per endpoint.

SettingDefaultRangeEffect
PollTimeoutSeconds51–3600How long a single poll waits for messages before returning empty
MaxPollMessages321–1024Maximum messages fetched per poll batch
Per-endpoint poll tuning
cfg.ReceiveEndpoint("order-processing", e =>
{
    e.PollTimeoutSeconds = 10;   // wait up to 10s per poll
    e.MaxPollMessages = 64;      // fetch up to 64 messages per poll
});

A longer PollTimeoutSeconds reduces idle gRPC traffic on quiet queues; a larger MaxPollMessages increases throughput on busy queues and supports larger batch consumers, at the cost of more in-flight messages per poll.

Errors and next steps

  • Faulted/skipped message routing, native DLQ via UseNativeDlq, and the retry pipeline → Error Handling & DLQ.
  • The full transport-options table and exception hierarchy → reference.

Was this page helpful?

On this page