Repository: JasperFx/wolverine (2026-07-18T21:26:26Z)

Issue:
Azure Service Bus: surface PrefetchCount
The Azure Service Bus transport never sets `ServiceBusReceiverOptions.PrefetchCount` — listeners configure `MaximumMessagesToReceive` / `MaximumWaitTime`, but the client-side prefetch buffer stays at its default of 0. For high-throughput queues this is the single cheapest throughput knob in the transport.

## Tasks

- [ ] Expose per-endpoint configuration (e.g. `listener.PrefetchCount(n)`) plus a transport-wide default
- [ ] Apply to session receivers too (`ServiceBusSessionReceiverOptions`), if sensible
- [ ] Docs: include the standard ASB caveat — prefetched messages age against the lock duration, so oversized prefetch + slow handlers = lock-lost redeliveries; give sizing guidance relative to `MaximumMessagesToReceive` and handler latency



Discussion:

Patch:
diff --git a/docs/guide/messaging/transports/azureservicebus/conventional-routing.md b/docs/guide/messaging/transports/azureservicebus/conventional-routing.md
index 00b39307f..b86114873 100644
--- a/docs/guide/messaging/transports/azureservicebus/conventional-routing.md
+++ b/docs/guide/messaging/transports/azureservicebus/conventional-routing.md
@@ -54,7 +54,7 @@ builder.UseWolverine(opts =>
 using var host = builder.Build();
 await host.StartAsync();
 ```
-<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs#L578-L622' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_conventional_routing_for_azure_service_bus' title='Start of snippet'>anchor</a></sup>
+<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs#L614-L658' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_conventional_routing_for_azure_service_bus' title='Start of snippet'>anchor</a></sup>
 <!-- endSnippet -->
 
 ## Route to Topics and Subscriptions <Badge type="tip" text="1.6.0" />
diff --git a/docs/guide/messaging/transports/azureservicebus/deadletterqueues.md b/docs/guide/messaging/transports/azureservicebus/deadletterqueues.md
index c8afb1400..7ce4dba23 100644
--- a/docs/guide/messaging/transports/azureservicebus/deadletterqueues.md
+++ b/docs/guide/messaging/transports/azureservicebus/deadletterqueues.md
@@ -30,7 +30,7 @@ builder.UseWolverine(opts =>
 using var host = builder.Build();
 await host.StartAsync();
 ```
-<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs#L720-L741' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_asb_inline_dlq' title='Start of snippet'>anchor</a></sup>
+<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs#L756-L777' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_asb_inline_dlq' title='Start of snippet'>anchor</a></sup>
 <!-- endSnippet -->
 
 ### Buffered Endpoints
@@ -62,7 +62,7 @@ builder.UseWolverine(opts =>
 using var host = builder.Build();
 await host.StartAsync();
 ```
-<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs#L746-L768' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_asb_buffered_dlq' title='Start of snippet'>anchor</a></sup>
+<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs#L782-L804' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_asb_buffered_dlq' title='Start of snippet'>anchor</a></sup>
 <!-- endSnippet -->
 
 ### Durable Endpoints
@@ -93,7 +93,7 @@ builder.UseWolverine(opts =>
 using var host = builder.Build();
 await host.StartAsync();
 ```
-<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs#L773-L794' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_asb_durable_dlq' title='Start of snippet'>anchor</a></sup>
+<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs#L809-L830' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_asb_durable_dlq' title='Start of snippet'>anchor</a></sup>
 <!-- endSnippet -->
 
 ## Disabling Dead Letter Queues
@@ -121,7 +121,7 @@ builder.UseWolverine(opts =>
 using var host = builder.Build();
 await host.StartAsync();
 ```
-<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs#L799-L819' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_disable_asb_dlq' title='Start of snippet'>anchor</a></sup>
+<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs#L835-L855' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_disable_asb_dlq' title='Start of snippet'>anchor</a></sup>
 <!-- endSnippet -->
 
 ## Recovering Native Dead Letters to Durable Storage <Badge type="tip" text="6.9" />
diff --git a/docs/guide/messaging/transports/azureservicebus/index.md b/docs/guide/messaging/transports/azureservicebus/index.md
index 8327cb853..dbd830c28 100644
--- a/docs/guide/messaging/transports/azureservicebus/index.md
+++ b/docs/guide/messaging/transports/azureservicebus/index.md
@@ -161,7 +161,7 @@ builder.UseWolverine(opts =>
 
 });
 ```
-<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs#L379-L401' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_enabling_azure_service_bus_control_queues' title='Start of snippet'>anchor</a></sup>
+<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs#L415-L437' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_enabling_azure_service_bus_control_queues' title='Start of snippet'>anchor</a></sup>
 <!-- endSnippet -->
 
 ## Disabling System Queues
diff --git a/docs/guide/messaging/transports/azureservicebus/listening.md b/docs/guide/messaging/transports/azureservicebus/listening.md
index 08b3e0515..d82d9dbc4 100644
--- a/docs/guide/messaging/transports/azureservicebus/listening.md
+++ b/docs/guide/messaging/transports/azureservicebus/listening.md
@@ -101,6 +101,79 @@ properties are honored. Session-based listeners (`RequireSessions()`) do not use
 are not affected by this option.
 :::
 
+## Client-Side Prefetch
+
+For high-throughput endpoints, the single cheapest knob in this transport is the Azure Service Bus
+client's *prefetch* buffer. By default prefetch is disabled (`PrefetchCount = 0`), so every
+`ReceiveMessagesAsync()` round trip has to go all the way to the broker before any message can be
+handled. Setting a positive `PrefetchCount` has the Azure Service Bus client eagerly stream messages
+into a local buffer in the background, so the listener's receive calls are typically satisfied
+immediately from memory.
+
+You can set a prefetch count on any queue or subscription listener, and/or set a transport-wide
+default that every Azure Service Bus listening endpoint inherits unless it overrides the value
+itself:
+
+<!-- snippet: sample_configuring_azure_service_bus_prefetch_count -->
+<a id='snippet-sample_configuring_azure_service_bus_prefetch_count'></a>
+```cs
+var builder = Host.CreateApplicationBuilder();
+builder.UseWolverine(opts =>
+{
+    // One way or another, you're probably pulling the Azure Service Bus
+    // connection string out of configuration
+    var azureServiceBusConnectionString = builder
+        .Configuration
+        .GetConnectionString("azure-service-bus")!;
+
+    opts.UseAzureServiceBus(azureServiceBusConnectionString).AutoProvision()
+
+        // Optionally set a transport-wide default prefetch count that every
+        // Azure Service Bus listener will inherit unless overridden
+        .PrefetchCount(50);
+
+    opts.ListenToAzureServiceBusQueue("incoming")
+
+        // Have the Azure Service Bus client eagerly buffer up to 100 messages
+        // on the client for just this queue, overriding the transport default.
+        // Size this relative to MaximumMessagesToReceive and how fast your
+        // handlers actually are -- prefetched messages age against the message
+        // lock duration while they wait in the client buffer!
+        .PrefetchCount(100)
+        .MaximumMessagesToReceive(100)
+        .BufferedInMemory();
+});
+
+using var host = builder.Build();
+await host.StartAsync();
+```
+<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs#L316-L347' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_configuring_azure_service_bus_prefetch_count' title='Start of snippet'>anchor</a></sup>
+<!-- endSnippet -->
+
+The configured prefetch count is applied to the `ServiceBusReceiver` used by buffered and durable
+listeners, to the `ServiceBusSessionReceiver` used by session-based (`RequireSessions()`) listeners,
+and as the initial `ServiceBusProcessorOptions.PrefetchCount` for inline (`ProcessInline()`)
+listeners — where `ConfigureProcessor()` can still override it.
+
+::: warning
+Prefetched messages are locked for this consumer the moment the client buffers them, and their lock
+duration keeps ticking while they sit in the client-side buffer waiting for your handlers. If the
+prefetch buffer is oversized relative to how fast the endpoint actually processes messages, messages
+at the back of the buffer expire their locks before they are ever handled, and Azure Service Bus
+redelivers them — producing duplicate processing and, eventually, dead-lettered messages that were
+never really failing.
+
+Size `PrefetchCount` so that the *entire* prefetched batch can be processed comfortably within the
+queue's lock duration:
+
+- A good starting point is the Azure guidance of a small multiple (1-3x) of `MaximumMessagesToReceive`
+  (Wolverine's default is 20), rather than some huge number.
+- Estimate `(PrefetchCount + MaximumMessagesToReceive) * average handler latency` and keep that
+  figure well under the queue's or subscription's lock duration (30 seconds by default).
+- Fast handlers (a few milliseconds) can profitably use large prefetch buffers; slow handlers
+  (hundreds of milliseconds or more) should use a small prefetch count or leave prefetch disabled.
+:::
+
 ## Conventional Listener Configuration
 
 In the case of listening to a large number of queues, it may be beneficial
@@ -129,7 +202,7 @@ builder.UseWolverine(opts =>
 using var host = builder.Build();
 await host.StartAsync();
 ```
-<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs#L498-L519' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_conventional_listener_configuration_for_azure_service_bus' title='Start of snippet'>anchor</a></sup>
+<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs#L534-L555' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_conventional_listener_configuration_for_azure_service_bus' title='Start of snippet'>anchor</a></sup>
 <!-- endSnippet -->
 
 Note that any of these settings would be overridden by specific configuration to
diff --git a/docs/guide/messaging/transports/azureservicebus/publishing.md b/docs/guide/messaging/transports/azureservicebus/publishing.md
index 99a3d8334..92d6322f7 100644
--- a/docs/guide/messaging/transports/azureservicebus/publishing.md
+++ b/docs/guide/messaging/transports/azureservicebus/publishing.md
@@ -28,7 +28,7 @@ builder.UseWolverine(opts =>
 using var host = builder.Build();
 await host.StartAsync();
 ```
-<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs#L439-L462' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_publishing_to_specific_azure_service_bus_queue' title='Start of snippet'>anchor</a></sup>
+<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs#L475-L498' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_publishing_to_specific_azure_service_bus_queue' title='Start of snippet'>anchor</a></sup>
 <!-- endSnippet -->
 
 
@@ -60,7 +60,7 @@ builder.UseWolverine(opts =>
 using var host = builder.Build();
 await host.StartAsync();
 ```
-<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs#L524-L545' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_conventional_subscriber_configuration_for_azure_service_bus' title='Start of snippet'>anchor</a></sup>
+<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs#L560-L581' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_conventional_subscriber_configuration_for_azure_service_bus' title='Start of snippet'>anchor</a></sup>
 <!-- endSnippet -->
 
 Note that any of these settings would be overridden by specific configuration to
diff --git a/docs/guide/messaging/transports/azureservicebus/topics.md b/docs/guide/messaging/transports/azureservicebus/topics.md
index cbb81aab4..264057605 100644
--- a/docs/guide/messaging/transports/azureservicebus/topics.md
+++ b/docs/guide/messaging/transports/azureservicebus/topics.md
@@ -62,7 +62,7 @@ opts.ListenToAzureServiceBusSubscription(
         })
     .FromTopic("topic1");
 ```
-<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs#L361-L370' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_configuring_azure_service_bus_subscription_filter' title='Start of snippet'>anchor</a></sup>
+<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs#L397-L406' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_configuring_azure_service_bus_subscription_filter' title='Start of snippet'>anchor</a></sup>
 <!-- endSnippet -->
 
 The default filter if not customized is a simple `1=1` (always true) filter.
diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusConfiguration.cs b/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusConfiguration.cs
index 6e2df301f..12aa51ad0 100644
--- a/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusConfiguration.cs
+++ b/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusConfiguration.cs
@@ -43,6 +43,22 @@ public AzureServiceBusConfiguration DeleteAllExistingObjectsOnStartup()
         return this;
     }
 
+    /// <summary>
+    ///     Set the transport-wide default for the number of messages that the underlying Azure
+    ///     Service Bus receivers eagerly buffer on the client ahead of processing. Applies to every
+    ///     Azure Service Bus listening endpoint that does not override PrefetchCount itself. The
+    ///     default is 0 (prefetch is disabled). Prefetched messages age against the message lock
+    ///     duration while they sit in the client buffer, so size this relative to
+    ///     MaximumMessagesToReceive and your handler latency
+    /// </summary>
+    /// <param name="prefetchCount">The client-side prefetch count. Must be non-negative</param>
+    /// <returns></returns>
+    public AzureServiceBusConfiguration PrefetchCount(int prefetchCount)
+    {
+        Transport.PrefetchCount = prefetchCount;
+        return this;
+    }
+
     /// <summary>
     /// Override the sending logic behavior for unknown or missing tenant ids when
     /// using multi-tenanted namespaces
diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusQueueListenerConfiguration.cs b/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusQueueListenerConfiguration.cs
index e82e59a3a..af9a05aea 100644
--- a/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusQueueListenerConfiguration.cs
+++ b/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusQueueListenerConfiguration.cs
@@ -103,6 +103,28 @@ public AzureServiceBusQueueListenerConfiguration MaximumWaitTime(TimeSpan time)
         return this;
     }
 
+    /// <summary>
+    ///     The number of messages that the underlying Azure Service Bus receiver eagerly buffers
+    ///     on the client ahead of processing for this queue. The default is 0 (prefetch is
+    ///     disabled), or the transport-wide default set through
+    ///     <c>UseAzureServiceBus(...).PrefetchCount()</c>. Prefetched messages age against the
+    ///     queue's message lock duration while they sit in the client buffer, so size this
+    ///     relative to MaximumMessagesToReceive and your handler latency
+    /// </summary>
+    /// <param name="prefetchCount">The client-side prefetch count. Must be non-negative</param>
+    /// <returns></returns>
+    public AzureServiceBusQueueListenerConfiguration PrefetchCount(int prefetchCount)
+    {
+        if (prefetchCount < 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(prefetchCount), prefetchCount,
+                "PrefetchCount cannot be negative");
+        }
+
+        add(e => e.PrefetchCount = prefetchCount);
+        return this;
+    }
+
     /// <summary>
     /// Completely disable all SQS dead letter queueing for just this queue
     /// </summary>
diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusSubscriptionListenerConfiguration.cs b/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusSubscriptionListenerConfiguration.cs
index 1cd40a1f5..f76fcf718 100644
--- a/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusSubscriptionListenerConfiguration.cs
+++ b/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusSubscriptionListenerConfiguration.cs
@@ -126,6 +126,28 @@ public AzureServiceBusSubscriptionListenerConfiguration MaximumWaitTime(TimeSpan
         return this;
     }
 
+    /// <summary>
+    ///     The number of messages that the underlying Azure Service Bus receiver eagerly buffers
+    ///     on the client ahead of processing for this subscription. The default is 0 (prefetch is
+    ///     disabled), or the transport-wide default set through
+    ///     <c>UseAzureServiceBus(...).PrefetchCount()</c>. Prefetched messages age against the
+    ///     subscription's message lock duration while they sit in the client buffer, so size this
+    ///     relative to MaximumMessagesToReceive and your handler latency
+    /// </summary>
+    /// <param name="prefetchCount">The client-side prefetch count. Must be non-negative</param>
+    /// <returns></returns>
+    public AzureServiceBusSubscriptionListenerConfiguration PrefetchCount(int prefetchCount)
+    {
+        if (prefetchCount < 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(prefetchCount), prefetchCount,
+                "PrefetchCount cannot be negative");
+        }
+
+        add(e => e.PrefetchCount = prefetchCount);
+        return this;
+    }
+
     /// <summary>
     /// Force this subscription listener to require session identifiers. Use this for FIFO semantics
     /// </summary>
diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusTransport.Listening.cs b/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusTransport.Listening.cs
index 9edc3374e..c7df4c8a0 100644
--- a/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusTransport.Listening.cs
+++ b/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusTransport.Listening.cs
@@ -14,13 +14,14 @@ internal Task<ServiceBusSessionReceiver> AcceptNextSessionAsync(AzureServiceBusE
     {
         if (endpoint is AzureServiceBusQueue queue)
         {
-            return BusClient.AcceptNextSessionAsync(queue.QueueName, cancellationToken: cancellationToken);
+            return BusClient.AcceptNextSessionAsync(queue.QueueName, BuildSessionReceiverOptions(queue),
+                cancellationToken);
         }
 
         if (endpoint is AzureServiceBusSubscription subscription)
         {
             return BusClient.AcceptNextSessionAsync(subscription.Topic.TopicName, subscription.SubscriptionName,
-                cancellationToken: cancellationToken);
+                BuildSessionReceiverOptions(subscription), cancellationToken);
         }
 
         throw new ArgumentOutOfRangeException(nameof(endpoint),
@@ -78,7 +79,7 @@ private async Task<IListener> buildListenerForQueue(IWolverineRuntime runtime, I
             return inlineListener;
         }
 
-        var messageReceiver = BusClient.CreateReceiver(queue.QueueName);
+        var messageReceiver = BusClient.CreateReceiver(queue.QueueName, BuildReceiverOptions(queue));
         var logger = runtime.LoggerFactory.CreateLogger<BatchedAzureServiceBusListener>();
         var listener = new BatchedAzureServiceBusListener(queue, logger, receiver, messageReceiver, mapper, requeue);
 
@@ -132,7 +133,8 @@ private async Task<IListener> buildListenerForSubscription(IWolverineRuntime run
             return inlineListener;
         }
 
-        var messageReceiver = BusClient.CreateReceiver(subscription.Topic.TopicName, subscription.SubscriptionName);
+        var messageReceiver = BusClient.CreateReceiver(subscription.Topic.TopicName, subscription.SubscriptionName,
+            BuildReceiverOptions(subscription));
 
         var listener = new BatchedAzureServiceBusListener(subscription, runtime.LoggerFactory.CreateLogger<BatchedAzureServiceBusListener>(), receiver, messageReceiver, mapper, requeue);
 
@@ -147,7 +149,13 @@ private async Task<IListener> buildListenerForSubscription(IWolverineRuntime run
     // dead lettering and deferral.
     internal static ServiceBusProcessorOptions BuildProcessorOptions(AzureServiceBusEndpoint endpoint)
     {
-        var options = new ServiceBusProcessorOptions();
+        var options = new ServiceBusProcessorOptions
+        {
+            // Seeded from the endpoint (or the transport-wide default), but deliberately applied
+            // before ConfigureProcessor so a user customization can still override it for inline
+            // listeners.
+            PrefetchCount = endpoint.PrefetchCount
+        };
 
         endpoint.ConfigureProcessor?.Invoke(options);
 
@@ -157,4 +165,24 @@ internal static ServiceBusProcessorOptions BuildProcessorOptions(AzureServiceBus
 
         return options;
     }
+
+    // Builds the ServiceBusReceiverOptions for the batched (buffered/durable) listeners, carrying
+    // the endpoint's client-side prefetch configuration onto the receiver
+    internal static ServiceBusReceiverOptions BuildReceiverOptions(AzureServiceBusEndpoint endpoint)
+    {
+        return new ServiceBusReceiverOptions
+        {
+            PrefetchCount = endpoint.PrefetchCount
+        };
+    }
+
+    // Builds the ServiceBusSessionReceiverOptions for session-based listeners, carrying the
+    // endpoint's client-side prefetch configuration onto the session receiver
+    internal static ServiceBusSessionReceiverOptions BuildSessionReceiverOptions(AzureServiceBusEndpoint endpoint)
+    {
+        return new ServiceBusSessionReceiverOptions
+        {
+            PrefetchCount = endpoint.PrefetchCount
+        };
+    }
 }
\ No newline at end of file
diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusTransport.cs b/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusTransport.cs
index 302c98f20..c95ec8355 100644
--- a/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusTransport.cs
+++ b/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusTransport.cs
@@ -93,6 +93,31 @@ public override string SanitizeIdentifier(string identifier)
     /// </summary>
     public bool SystemQueuesEnabled { get; set; } = true;
 
+    private int _prefetchCount;
+
+    /// <summary>
+    ///     The transport-wide default for the number of messages that the underlying Azure Service
+    ///     Bus receivers eagerly buffer on the client ahead of any ReceiveMessagesAsync() calls.
+    ///     Applied to every listening endpoint that does not override PrefetchCount itself. The
+    ///     default is 0 (prefetch is disabled). Be aware that prefetched messages age against the
+    ///     queue's message lock duration while they sit in the client buffer, so an oversized
+    ///     prefetch combined with slow handlers leads to lock-lost redeliveries.
+    /// </summary>
+    public int PrefetchCount
+    {
+        get => _prefetchCount;
+        set
+        {
+            if (value < 0)
+            {
+                throw new ArgumentOutOfRangeException(nameof(value), value,
+                    "PrefetchCount cannot be negative");
+            }
+
+            _prefetchCount = value;
+        }
+    }
+
     [IgnoreDescription]
     public LightweightCache<string, AzureServiceBusQueue> Queues { get; }
     [IgnoreDescription]
diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusEndpoint.cs b/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusEndpoint.cs
index 8e25daf69..20132b0e3 100644
--- a/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusEndpoint.cs
+++ b/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusEndpoint.cs
@@ -23,10 +23,21 @@ public interface IAzureServiceBusListeningEndpoint
     ///     with an empty list of messages. Default is 5 seconds.
     /// </summary>
     public TimeSpan MaximumWaitTime { get; set; }
+
+    /// <summary>
+    ///     The number of messages that the underlying Azure Service Bus receiver eagerly buffers
+    ///     on the client ahead of any ReceiveMessagesAsync() calls. The default is 0 (prefetch is
+    ///     disabled). Be aware that prefetched messages age against the queue's message lock
+    ///     duration while they sit in the client buffer, so an oversized prefetch combined with
+    ///     slow handlers leads to lock-lost redeliveries.
+    /// </summary>
+    public int PrefetchCount { get; set; }
 }
 
 public abstract class AzureServiceBusEndpoint : Endpoint<IAzureServiceBusEnvelopeMapper, AzureServiceBusEnvelopeMapper>, IBrokerEndpoint, IAzureServiceBusListeningEndpoint
 {
+    private int? _prefetchCount;
+
     public AzureServiceBusEndpoint(AzureServiceBusTransport parent, Uri uri, EndpointRole role) : base(uri, role)
     {
         Parent = parent;
@@ -49,6 +60,30 @@ public AzureServiceBusEndpoint(AzureServiceBusTransport parent, Uri uri, Endpoin
     /// </summary>
     public TimeSpan MaximumWaitTime { get; set; } = 5.Seconds();
 
+    /// <summary>
+    ///     The number of messages that the underlying Azure Service Bus receiver eagerly buffers
+    ///     on the client ahead of any ReceiveMessagesAsync() calls. Falls back to the transport-wide
+    ///     default (see AzureServiceBusTransport.PrefetchCount) unless explicitly set on this
+    ///     endpoint. The ultimate default is 0 (prefetch is disabled). Be aware that prefetched
+    ///     messages age against the queue's message lock duration while they sit in the client
+    ///     buffer, so an oversized prefetch combined with slow handlers leads to lock-lost
+    ///     redeliveries.
+    /// </summary>
+    public int PrefetchCount
+    {
+        get => _prefetchCount ?? Parent.PrefetchCount;
+        set
+        {
+            if (value < 0)
+            {
+                throw new ArgumentOutOfRangeException(nameof(value), value,
+                    "PrefetchCount cannot be negative");
+            }
+
+            _prefetchCount = value;
+        }
+    }
+
     /// <summary>
     ///     Optional customization of the Azure Service Bus <see cref="ServiceBusProcessorOptions" /> used
     ///     by inline listeners for this endpoint. Wolverine reserves control of the properties it depends


Test patch:
diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs
index f766ff356..a98cecc95 100644
--- a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs
+++ b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/DocumentationSamples.cs
@@ -311,6 +311,42 @@ public async Task configuring_processor_options()
         #endregion
     }
 
+    public async Task configuring_prefetch_count()
+    {
+        #region sample_configuring_azure_service_bus_prefetch_count
+        var builder = Host.CreateApplicationBuilder();
+        builder.UseWolverine(opts =>
+        {
+            // One way or another, you're probably pulling the Azure Service Bus
+            // connection string out of configuration
+            var azureServiceBusConnectionString = builder
+                .Configuration
+                .GetConnectionString("azure-service-bus")!;
+
+            opts.UseAzureServiceBus(azureServiceBusConnectionString).AutoProvision()
+
+                // Optionally set a transport-wide default prefetch count that every
+                // Azure Service Bus listener will inherit unless overridden
+                .PrefetchCount(50);
+
+            opts.ListenToAzureServiceBusQueue("incoming")
+
+                // Have the Azure Service Bus client eagerly buffer up to 100 messages
+                // on the client for just this queue, overriding the transport default.
+                // Size this relative to MaximumMessagesToReceive and how fast your
+                // handlers actually are -- prefetched messages age against the message
+                // lock duration while they wait in the client buffer!
+                .PrefetchCount(100)
+                .MaximumMessagesToReceive(100)
+                .BufferedInMemory();
+        });
+
+        using var host = builder.Build();
+        await host.StartAsync();
+
+        #endregion
+    }
+
     public async Task configure_buffered_listener()
     {
         var builder = Host.CreateApplicationBuilder();
diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/configuring_prefetch_count.cs b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/configuring_prefetch_count.cs
new file mode 100644
index 000000000..df47bdfcc
--- /dev/null
+++ b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/configuring_prefetch_count.cs
@@ -0,0 +1,150 @@
+using Shouldly;
+using Wolverine.Configuration;
+using Xunit;
+
+namespace Wolverine.AzureServiceBus.Tests;
+
+public class configuring_prefetch_count
+{
+    [Fact]
+    public void prefetch_is_disabled_by_default()
+    {
+        var transport = new AzureServiceBusTransport();
+
+        transport.PrefetchCount.ShouldBe(0);
+        transport.Queues["incoming"].PrefetchCount.ShouldBe(0);
+    }
+
+    [Fact]
+    public void fluent_configuration_flows_to_the_queue_endpoint()
+    {
+        var transport = new AzureServiceBusTransport();
+        var queue = transport.Queues["incoming"];
+
+        var configuration = new AzureServiceBusQueueListenerConfiguration(queue);
+        configuration.PrefetchCount(50).ShouldBeSameAs(configuration);
+
+        // Apply the delayed configuration as Wolverine would at bootstrapping time
+        ((IDelayedEndpointConfiguration)configuration).Apply();
+
+        queue.PrefetchCount.ShouldBe(50);
+    }
+
+    [Fact]
+    public void fluent_configuration_flows_to_the_subscription_endpoint()
+    {
+        var transport = new AzureServiceBusTransport();
+        var topic = transport.Topics["topic1"];
+        var subscription = topic.FindOrCreateSubscription("sub1");
+
+        var configuration = new AzureServiceBusSubscriptionListenerConfiguration(subscription);
+        configuration.PrefetchCount(25).ShouldBeSameAs(configuration);
+
+        ((IDelayedEndpointConfiguration)configuration).Apply();
+
+        subscription.PrefetchCount.ShouldBe(25);
+    }
+
+    [Fact]
+    public void transport_wide_default_is_inherited_by_endpoints()
+    {
+        var transport = new AzureServiceBusTransport();
+        var configuration = new AzureServiceBusConfiguration(transport, new WolverineOptions());
+
+        configuration.PrefetchCount(100).ShouldBeSameAs(configuration);
+
+        transport.PrefetchCount.ShouldBe(100);
+        transport.Queues["incoming"].PrefetchCount.ShouldBe(100);
+        transport.Topics["topic1"].FindOrCreateSubscription("sub1").PrefetchCount.ShouldBe(100);
+    }
+
+    [Fact]
+    public void endpoint_override_wins_over_the_transport_wide_default()
+    {
+        var transport = new AzureServiceBusTransport();
+        transport.PrefetchCount = 100;
+
+        var queue = transport.Queues["incoming"];
+        queue.PrefetchCount = 10;
+
+        queue.PrefetchCount.ShouldBe(10);
+
+        // Other endpoints still see the transport default
+        transport.Queues["other"].PrefetchCount.ShouldBe(100);
+    }
+
+    [Fact]
+    public void negative_values_are_rejected()
+    {
+        var transport = new AzureServiceBusTransport();
+        var queue = transport.Queues["incoming"];
+        var listenerConfiguration = new AzureServiceBusQueueListenerConfiguration(queue);
+        var subscription = transport.Topics["topic1"].FindOrCreateSubscription("sub1");
+        var subscriptionConfiguration = new AzureServiceBusSubscriptionListenerConfiguration(subscription);
+
+        Should.Throw<ArgumentOutOfRangeException>(() => transport.PrefetchCount = -1);
+        Should.Throw<ArgumentOutOfRangeException>(() => queue.PrefetchCount = -1);
+        Should.Throw<ArgumentOutOfRangeException>(() => listenerConfiguration.PrefetchCount(-1));
+        Should.Throw<ArgumentOutOfRangeException>(() => subscriptionConfiguration.PrefetchCount(-1));
+    }
+
+    [Fact]
+    public void build_receiver_options_carries_the_endpoint_prefetch_count()
+    {
+        var transport = new AzureServiceBusTransport();
+        var queue = transport.Queues["incoming"];
+        queue.PrefetchCount = 50;
+
+        var options = AzureServiceBusTransport.BuildReceiverOptions(queue);
+
+        options.PrefetchCount.ShouldBe(50);
+    }
+
+    [Fact]
+    public void build_receiver_options_carries_the_transport_default()
+    {
+        var transport = new AzureServiceBusTransport();
+        transport.PrefetchCount = 75;
+
+        var options = AzureServiceBusTransport.BuildReceiverOptions(transport.Queues["incoming"]);
+
+        options.PrefetchCount.ShouldBe(75);
+    }
+
+    [Fact]
+    public void build_session_receiver_options_carries_the_endpoint_prefetch_count()
+    {
+        var transport = new AzureServiceBusTransport();
+        var subscription = transport.Topics["topic1"].FindOrCreateSubscription("sub1");
+        subscription.PrefetchCount = 30;
+
+        var options = AzureServiceBusTransport.BuildSessionReceiverOptions(subscription);
+
+        options.PrefetchCount.ShouldBe(30);
+    }
+
+    [Fact]
+    public void build_processor_options_carries_the_endpoint_prefetch_count()
+    {
+        var transport = new AzureServiceBusTransport();
+        var queue = transport.Queues["incoming"];
+        queue.PrefetchCount = 40;
+
+        var options = AzureServiceBusTransport.BuildProcessorOptions(queue);
+
+        options.PrefetchCount.ShouldBe(40);
+    }
+
+    [Fact]
+    public void configure_processor_can_still_override_the_prefetch_count_for_inline_listeners()
+    {
+        var transport = new AzureServiceBusTransport();
+        var queue = transport.Queues["incoming"];
+        queue.PrefetchCount = 40;
+        queue.ConfigureProcessor = o => o.PrefetchCount = 5;
+
+        var options = AzureServiceBusTransport.BuildProcessorOptions(queue);
+
+        options.PrefetchCount.ShouldBe(5);
+    }
+}
diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/prefetch_count_end_to_end.cs b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/prefetch_count_end_to_end.cs
new file mode 100644
index 000000000..6801797d6
--- /dev/null
+++ b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/prefetch_count_end_to_end.cs
@@ -0,0 +1,84 @@
+using JasperFx.Core;
+using Microsoft.Extensions.Hosting;
+using Shouldly;
+using Wolverine.AzureServiceBus.Internal;
+using Wolverine.Tracking;
+using Xunit;
+
+namespace Wolverine.AzureServiceBus.Tests;
+
+public class prefetch_count_end_to_end : IAsyncLifetime
+{
+    private IHost _host = null!;
+
+    public async Task InitializeAsync()
+    {
+        _host = await Host.CreateDefaultBuilder()
+            .UseWolverine(opts =>
+            {
+                opts.UseAzureServiceBusTesting()
+                    .AutoProvision().AutoPurgeOnStartup()
+
+                    // Transport-wide default prefetch
+                    .PrefetchCount(10);
+
+                opts.ListenToAzureServiceBusQueue("prefetch1")
+
+                    // Endpoint override of the transport-wide default
+                    .PrefetchCount(20)
+                    .BufferedInMemory();
+
+                opts.PublishMessage<AsbPrefetchMessage>().ToAzureServiceBusQueue("prefetch1");
+            }).StartAsync();
+    }
+
+    public async Task DisposeAsync()
+    {
+        await _host.StopAsync();
+        _host.Dispose();
+        await AzureServiceBusTesting.DeleteAllEmulatorObjectsAsync();
+    }
+
+    [Fact]
+    public void prefetch_configuration_is_applied_to_the_endpoint()
+    {
+        var transport = _host.GetRuntime().Options.Transports.GetOrCreate<AzureServiceBusTransport>();
+
+        transport.PrefetchCount.ShouldBe(10);
+        transport.Queues["prefetch1"].PrefetchCount.ShouldBe(20);
+
+        // Any endpoint w/o an explicit override inherits the transport-wide default
+        transport.Queues["some.other.queue"].PrefetchCount.ShouldBe(10);
+    }
+
+    [Fact]
+    public async Task send_and_receive_through_a_prefetching_listener()
+    {
+        Func<IMessageContext, Task> sendMany = async bus =>
+        {
+            for (var i = 0; i < 25; i++)
+            {
+                await bus.SendAsync(new AsbPrefetchMessage(i));
+            }
+        };
+
+        var session = await _host.TrackActivity()
+            .IncludeExternalTransports()
+            .Timeout(60.Seconds())
+            .ExecuteAndWaitAsync(sendMany);
+
+        session.Received.MessagesOf<AsbPrefetchMessage>()
+            .Select(x => x.Number).OrderBy(x => x)
+            .ShouldBe(Enumerable.Range(0, 25));
+    }
+}
+
+public record AsbPrefetchMessage(int Number);
+
+public static class AsbPrefetchMessageHandler
+{
+    public static void Handle(AsbPrefetchMessage message)
+    {
+        // nothing
+    }
+}