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
| Member | Where | Purpose |
|---|---|---|
bus.GetSendEndpoint(Uri) / IKubeMQRider.GetSendEndpoint(Uri, ct) | bus / rider | Resolve a send endpoint for a queue: or kubemq://host:port/channel address |
ISendEndpoint.Send<T>(message, ctx => …) | send endpoint | Send a message; the context pipe sets Delay, TimeToLive, and headers |
SendContext.Delay | send context | Delayed delivery → QueueMessage.DelaySeconds |
SendContext.TimeToLive | send context | Per-message TTL → QueueMessage.ExpirationSeconds |
IKubeMQEndpointTransportConfigurator.ExpirationSeconds | ConfigureKubeMQ | Per-endpoint TTL for every message the endpoint produces |
IKubeMQBusFactoryConfigurator.UsePriorityQueues(…) | bus factory | Enable weighted _high/_normal/_low priority channels |
IKubeMQReceiveEndpointConfigurator.PollTimeoutSeconds / .MaxPollMessages | receive endpoint | Tune 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.
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.
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.
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);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.
// 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 BatchOptions — SetMessageLimit 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).
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.
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.
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:
// 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.
| Setting | Default | Range | Effect |
|---|---|---|---|
PollTimeoutSeconds | 5 | 1–3600 | How long a single poll waits for messages before returning empty |
MaxPollMessages | 32 | 1–1024 | Maximum messages fetched per poll batch |
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?
Observability
Wire up ASP.NET Core health checks, OpenTelemetry distributed tracing, and the MassTransit.KubeMQ metrics meter for the transport.
API Reference
MassTransit.KubeMQ registration entry points and the bus-factory, host, receive-endpoint, transport, and priority-queue configurator interfaces.