Performance Tuning
This page collects the levers that matter most for throughput and latency with the Amazon SQS transport, and the factors behind them.
The receive side: batches in, singles out
Wolverine receives from SQS in batches — each poll asks for up to MaxNumberOfMessages (default 10, the SQS maximum) with long polling enabled by default (WaitTimeSeconds = 5). Durable endpoints benefit doubly: the whole received batch is written to the database inbox in a single batched insert, so durable SQS endpoints are considerably cheaper per message than push-based transports.
Message completion is batched too. Completed messages accumulate for up to 50 milliseconds and are deleted with a single DeleteMessageBatch call of up to 10 — so a full 10-message receive is settled with one round trip instead of ten. Since SQS bills per API call, this is a cost lever as much as a latency one. The practical levers:
opts.ListenToSqsQueue("orders")
// Parallel pollers: N independent receive loops on the same queue.
// The main receive-side throughput lever today.
.ListenerCount(4)
// Long-poll duration. Raise toward 20s for low-traffic queues to cut
// empty-receive API calls (and cost); keep short only if you need
// faster listener shutdown.
.ConfigureListener(l => l.WaitTimeSeconds = 20)
// Delete batching. Defaults to 10 (the SQS maximum) with a 50ms window.
// Pass 1 to go back to one DeleteMessage call per message.
.DeleteMessageBatchSize(10, 50.Milliseconds())
.MaximumParallelMessages(10);TIP
The delete window is a maximum batch age, not a quiet period, so a single message never waits longer than the window. Batches are also flushed when a listener stops, so a paused listener does not leave settled messages to reappear at their visibility timeout.
Visibility timeout: size it against your processing window
Wolverine sets each received message's visibility timeout at receive (default 120 seconds) and does not extend it while messages wait in the local worker queue or execute. If (queued messages ÷ processing rate) + handler time can exceed the visibility timeout, SQS redelivers messages that are still in flight:
- Durable endpoints deduplicate redeliveries through the inbox — you pay wasted work, not duplicate side effects.
- Buffered endpoints delete messages from SQS as soon as they are buffered, before handling — so instead of duplicates you get at-most-once semantics: an ungraceful crash loses the buffered backlog.
- Inline endpoints delete after successful handling — safest, and the visibility timeout only needs to cover a single handler execution.
Rule of thumb: keep BufferingLimits.Maximum × average handler time ÷ MaximumParallelMessages comfortably under the visibility timeout, or raise the timeout on the queue.
The send side: batch API, one batch in flight
Wolverine sends with SendMessageBatch (10 messages per API call, the SQS maximum) through its batched sender. Two defaults are worth revisiting for high-volume publishers:
opts.PublishMessage<OrderPlaced>().ToSqsQueue("orders")
// Default is 1: only one batch API call in flight at a time per endpoint.
// Raising this is the single cheapest send-throughput lever for SQS.
.MessageBatchMaxDegreeOfParallelism(8)
// The batch timeout is a debounce (each new message resets it) —
// for low-rate latency-sensitive routes, shrink it from the 250ms default.
.MessageBatchTimeout(50.Milliseconds());With the defaults, sustained sending tops out around 10 messages per SQS round trip per endpoint. Since SQS bills per API call, batching efficiency is also directly a cost lever.
Payload size
The default envelope mapper embeds the serialized Wolverine envelope in the message body as Base64, which inflates the wire size by roughly a third — budget against the 256 KB SQS message limit accordingly. For large or high-volume payloads where you control both ends, the raw JSON mapper (or a custom ISqsEnvelopeMapper) avoids the Base64 wrapping.
SQS also caps an entire SendMessageBatch request at 256 KB, not just each message in it, so ten individually legal 30 KB messages would bounce the whole request. Wolverine chunks outgoing batches on both limits — ten entries and the request payload budget — so large messages simply produce smaller batches.
FIFO queues
FIFO queues give broker-side ordering per MessageGroupId (mapped automatically from Wolverine's Envelope.GroupId), but two caveats:
- FIFO throughput is limited per message group — total throughput scales with the number of distinct group ids, so a workload funneled into a few groups will hit SQS's FIFO caps long before the transport is the bottleneck.
- Broker-side ordering does not by itself serialize processing: Buffered/Durable endpoints execute a received batch in parallel. Pair FIFO listening with
PartitionProcessingByGroupId(...)(or Inline mode) to preserve per-group ordering end to end.
On standard queues, PartitionProcessingByGroupId(...) alone gives per-key sequential processing within a node without the FIFO throughput caps, and UseShardedAmazonSqsQueues(...) in a global partitioned topology adds cluster-wide ordering at the cost of forced durable endpoints.
Native scheduled delivery via DelaySeconds is used automatically for delays up to 15 minutes on standard queues; longer delays or FIFO queues fall back to Wolverine's database-backed scheduling.
Interpreting Wolverine's metrics
wolverine-execution-time measures the handler plus all middleware (including time blocked inside middleware); wolverine-effective-time is wall-clock from the sender's SentAt stamp through handling, cascading-message flush, and completion, and is sensitive to clock skew across machines.

