{"id":3780,"date":"2026-04-28T11:47:37","date_gmt":"2026-04-28T18:47:37","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/azure-sdk\/?p=3780"},"modified":"2026-04-28T11:47:37","modified_gmt":"2026-04-28T18:47:37","slug":"per-message-settlement-azure-service-bus","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/azure-sdk\/per-message-settlement-azure-service-bus\/","title":{"rendered":"The problem: All-or-nothing batch processing in Azure Service Bus"},"content":{"rendered":"<p>Azure Service Bus is one of the most widely used messaging services for building event-driven applications on Azure. When you use Azure Functions with a Service Bus trigger in batch mode, your function receives multiple messages at once for efficient, high-throughput processing.<\/p>\n<p>But what happens when one message in the batch fails? Assume your function receives a batch of 50 Service Bus messages. Forty-nine process perfectly, and one fails.<\/p>\n<p>In the default model, the entire batch fails. All 50 messages go back on the queue and get reprocessed, including the 49 that already succeeded. This leads to:<\/p>\n<ul>\n<li><strong>Duplicate processing<\/strong>\u2014messages that were already handled successfully get processed again<\/li>\n<li><strong>Wasted compute<\/strong>\u2014you pay for re-executing work that already completed<\/li>\n<li><strong>Infinite retry loops<\/strong>\u2014if that one &#8220;poison&#8221; message keeps failing, it blocks the entire batch indefinitely<\/li>\n<li><strong>Idempotency burden<\/strong>\u2014your downstream systems must handle duplicates gracefully, adding complexity to every consumer<\/li>\n<\/ul>\n<p>This pattern is the classic all-or-nothing batch failure problem. Azure Functions solves it with per-message settlement.<\/p>\n<h2>The solution: Per-message settlement for Azure Service Bus<\/h2>\n<p>Azure Functions gives you direct control over how each individual message is settled in real time, as you process it. Instead of treating the batch as all-or-nothing, you settle each message independently based on its processing outcome.<\/p>\n<p>With Service Bus message settlement actions in Azure Functions, you can:<\/p>\n<table>\n<thead>\n<tr>\n<th>Action<\/th>\n<th>What It Does<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Complete<\/td>\n<td>Remove the message from the queue (successfully processed)<\/td>\n<\/tr>\n<tr>\n<td>Abandon<\/td>\n<td>Release the lock so the message returns to the queue for retry, optionally modifying application properties<\/td>\n<\/tr>\n<tr>\n<td>Dead-letter<\/td>\n<td>Move the message to the dead-letter queue (poison message handling)<\/td>\n<\/tr>\n<tr>\n<td>Defer<\/td>\n<td>Keep the message in the queue but make it only retrievable by sequence number<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>This capability means in a batch of 50 messages, you can:<\/p>\n<ul>\n<li>Complete 47 that processed successfully<\/li>\n<li>Abandon 2 that hit a transient error (with updated retry metadata)<\/li>\n<li>Dead-letter 1 that&#8217;s malformed and never succeeds<\/li>\n<\/ul>\n<p>All in a single function invocation. No reprocessing of successful messages. No building failure response objects. No all-or-nothing.<\/p>\n<h2>Why this matters<\/h2>\n<h3>1. Eliminates duplicate processing<\/h3>\n<p>When you complete messages individually, successfully processed messages are immediately removed from the queue. There&#8217;s no chance of them being redelivered, even if other messages in the same batch fail.<\/p>\n<h3>2. Enables granular error handling<\/h3>\n<p>Different failures deserve different treatments. A malformed message should be dead-lettered immediately. A message that failed due to a transient database timeout should be abandoned for retry. A message that requires manual intervention should be deferred. Per-message settlement gives you this granularity.<\/p>\n<h3>3. Implements exponential backoff without external infrastructure<\/h3>\n<p>By combining abandon with modified application properties, you can track retry counts per message, and implement exponential backoff patterns directly in your function code. No extra queues or Durable Functions are required.<\/p>\n<h3>4. Reduces cost<\/h3>\n<p>You stop paying for redundant re-execution of already-successful work. In high-throughput systems processing millions of messages, this approach can be a material cost reduction.<\/p>\n<h3>5. Simplifies idempotency requirements<\/h3>\n<p>When successful messages are never redelivered, your downstream systems don&#8217;t need to guard against duplicates as aggressively. This change reduces architectural complexity and potential for bugs.<\/p>\n<h2>Before: One message = one function invocation<\/h2>\n<p>Before batch support, there was no cardinality option, Azure Functions processed each Service Bus message as a separate function invocation. If your queue had 50 messages, the runtime spun up 50 individual executions.<\/p>\n<h3>Single-message processing (the old way)<\/h3>\n<pre><code class=\"language-typescript\">import { app, InvocationContext } from '@azure\/functions';\r\n\r\nasync function processOrder(\r\n    message: unknown,  \/\/ \u2190 One message at a time, no batch\r\n    context: InvocationContext\r\n): Promise&lt;void&gt; {\r\n    try {\r\n        const order = message as Order;\r\n        await processOrder(order);\r\n    } catch (error) {\r\n        context.error('Failed to process message:', error);\r\n        \/\/ Message auto-complete by default.\r\n        throw error;\r\n    }\r\n}\r\n\r\napp.serviceBusQueue('processOrder', {\r\n    connection: 'ServiceBusConnection',\r\n    queueName: 'orders-queue',\r\n    handler: processOrder,\r\n});<\/code><\/pre>\n<p>What this cost you:<\/p>\n<table>\n<thead>\n<tr>\n<th>50 messages on the queue<\/th>\n<th>Old (single-message)<\/th>\n<th>New (batch + settlement)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Function invocations<\/td>\n<td>50 separate invocations<\/td>\n<td>One invocation<\/td>\n<\/tr>\n<tr>\n<td>Connection overhead<\/td>\n<td>50 separate DB\/API connections<\/td>\n<td>One connection, reused across batch<\/td>\n<\/tr>\n<tr>\n<td>Compute cost<\/td>\n<td>50\u00d7 invocation overhead<\/td>\n<td>1\u00d7 invocation overhead<\/td>\n<\/tr>\n<tr>\n<td>Settlement control<\/td>\n<td>Binary: throw or don&#8217;t<\/td>\n<td>Four actions per message<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Every message paid the full price of a function invocation, startup, connection setup, teardown. At scale (millions of messages\/day), this overhead was a significant cost and latency penalty. And when a message failed, your only option was to throw (retry the whole message) or swallow the error (lose it silently).<\/p>\n<h2>Code examples<\/h2>\n<p>Let&#8217;s see how this looks across all three major Azure Functions language stacks.<\/p>\n<h3>Node.js (TypeScript with @azure\/functions-extensions-servicebus)<\/h3>\n<pre><code class=\"language-typescript\">import '@azure\/functions-extensions-servicebus';\r\nimport { app, InvocationContext } from '@azure\/functions';\r\nimport { ServiceBusMessageContext, messageBodyAsJson } from '@azure\/functions-extensions-servicebus';\r\n\r\ninterface Order { id: string; product: string; amount: number; }\r\n\r\nexport async function processOrderBatch(\r\n    sbContext: ServiceBusMessageContext,\r\n    context: InvocationContext\r\n): Promise&lt;void&gt; {\r\n    const { messages, actions } = sbContext;\r\n\r\n    for (const message of messages) {\r\n        try {\r\n            const order = messageBodyAsJson&lt;Order&gt;(message);\r\n            await processOrder(order);\r\n            await actions.complete(message);            \/\/ \u2705 Done\r\n        } catch (error) {\r\n            context.error(`Failed ${message.messageId}:`, error);\r\n            await actions.deadletter(message);          \/\/ \u2620\ufe0f Poison\r\n        }\r\n    }\r\n}\r\n\r\napp.serviceBusQueue('processOrderBatch', {\r\n    connection: 'ServiceBusConnection',\r\n    queueName: 'orders-queue',\r\n    sdkBinding: true,\r\n    autoCompleteMessages: false,\r\n    cardinality: 'many',\r\n    handler: processOrderBatch,\r\n});<\/code><\/pre>\n<p><strong>Key points:<\/strong><\/p>\n<ul>\n<li>Enable <code>sdkBinding: true<\/code> and <code>autoCompleteMessages: false<\/code> to gain manual settlement control<\/li>\n<li><code>ServiceBusMessageContext<\/code> provides both the <code>messages<\/code> array and <code>actions<\/code> object<\/li>\n<li>Settlement actions: <code>complete()<\/code>, <code>abandon()<\/code>, <code>deadletter()<\/code>, <code>defer()<\/code><\/li>\n<li>Application properties can be passed to <code>abandon()<\/code> for retry tracking<\/li>\n<li>Built-in helpers like <code>messageBodyAsJson&lt;T&gt;()<\/code> handle Buffer-to-object parsing<\/li>\n<\/ul>\n<p>Full sample: <a href=\"https:\/\/github.com\/Azure\/azure-functions-nodejs-extensions\/tree\/main\/azure-functions-nodejs-extensions-servicebus\/samples\/serviceBusSampleWithComplete\">serviceBusSampleWithComplete<\/a><\/p>\n<h3>Python (V2 programming model)<\/h3>\n<pre><code class=\"language-python\">import json\r\nimport logging\r\nfrom typing import List\r\n\r\nimport azure.functions as func\r\nimport azurefunctions.extensions.bindings.servicebus as servicebus\r\n\r\napp = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)\r\n\r\n@app.service_bus_queue_trigger(arg_name=\"messages\",\r\n                               queue_name=\"orders-queue\",\r\n                               connection=\"SERVICEBUS_CONNECTION\",\r\n                               auto_complete_messages=False,\r\n                               cardinality=\"many\")\r\ndef process_order_batch(messages: List[servicebus.ServiceBusReceivedMessage],\r\n                        message_actions: servicebus.ServiceBusMessageActions):\r\n    for message in messages:\r\n        try:\r\n            order = json.loads(message.body)\r\n            process_order(order)\r\n            message_actions.complete(message)              # \u2705 Done\r\n        except Exception as e:\r\n            logging.error(f\"Failed {message.message_id}: {e}\")\r\n            message_actions.dead_letter(message)           # \u2620\ufe0f Poison\r\n\r\ndef process_order(order):\r\n    logging.info(f\"Processing order: {order['id']}\")<\/code><\/pre>\n<p><strong>Key points:<\/strong><\/p>\n<ul>\n<li>Uses <code>azurefunctions.extensions.bindings.servicebus<\/code> for SDK-type bindings with <code>ServiceBusReceivedMessage<\/code><\/li>\n<li>The extension supports both queue and topic triggers with <code>cardinality=\"many\"<\/code> for batch processing<\/li>\n<li>Each message exposes SDK properties like <code>body<\/code>, <code>enqueued_time_utc<\/code>, <code>lock_token<\/code>, <code>message_id<\/code>, and <code>sequence_number<\/code><\/li>\n<\/ul>\n<p>Full sample: <a href=\"https:\/\/github.com\/Azure\/azure-functions-python-extensions\/tree\/dev\/azurefunctions-extensions-bindings-servicebus\/samples\/servicebus_samples_settlement\">servicebus_samples_settlement<\/a><\/p>\n<h3>.NET (C# Isolated Worker)<\/h3>\n<pre><code class=\"language-csharp\">using Azure.Messaging.ServiceBus;\r\nusing Microsoft.Azure.Functions.Worker;\r\n\r\npublic class ServiceBusBatchProcessor(ILogger&lt;ServiceBusBatchProcessor&gt; logger)\r\n{\r\n    [Function(nameof(ProcessOrderBatch))]\r\n    public async Task ProcessOrderBatch(\r\n        [ServiceBusTrigger(\"orders-queue\", Connection = \"ServiceBusConnection\")]\r\n        ServiceBusReceivedMessage[] messages,\r\n        ServiceBusMessageActions messageActions)\r\n    {\r\n        foreach (var message in messages)\r\n        {\r\n            try\r\n            {\r\n                var order = message.Body.ToObjectFromJson&lt;Order&gt;();\r\n                await ProcessOrder(order);\r\n                await messageActions.CompleteMessageAsync(message);       \/\/ \u2705 Done\r\n            }\r\n            catch (Exception ex)\r\n            {\r\n                logger.LogError(ex, \"Failed {MessageId}\", message.MessageId);\r\n                await messageActions.DeadLetterMessageAsync(message);     \/\/ \u2620\ufe0f Poison\r\n            }\r\n        }\r\n    }\r\n\r\n    private Task ProcessOrder(Order order) =&gt; Task.CompletedTask;\r\n}\r\n\r\npublic record Order(string Id, string Product, decimal Amount);<\/code><\/pre>\n<p><strong>Key points:<\/strong><\/p>\n<ul>\n<li>Inject <code>ServiceBusMessageActions<\/code> directly alongside the message array<\/li>\n<li>Each message is individually settled with <code>CompleteMessageAsync<\/code>, <code>DeadLetterMessageAsync<\/code>, or <code>AbandonMessageAsync<\/code><\/li>\n<li>Application properties can be modified on abandon to track retry metadata<\/li>\n<\/ul>\n<p>Full sample: <a href=\"https:\/\/github.com\/Azure\/azure-functions-dotnet-worker\/blob\/main\/samples\/Extensions\/ServiceBus\/ServiceBusReceivedMessageFunctions.cs\">ServiceBusReceivedMessageFunctions.cs<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Azure Functions lets you settle each Service Bus message on its own within a batch. Complete, abandon, dead-letter, or defer messages one by one to avoid duplicate processing and handle errors with precision.<\/p>\n","protected":false},"author":209626,"featured_media":3785,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[734,765,816,979,161,159,825,162,960,733],"class_list":["post-3780","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-azure-sdk","tag-azure","tag-azure-functions","tag-azure-service-bus","tag-batch-processing","tag-dotnet","tag-javascript","tag-messaging","tag-python","tag-serverless","tag-typescript"],"acf":[],"blog_post_summary":"<p>Azure Functions lets you settle each Service Bus message on its own within a batch. Complete, abandon, dead-letter, or defer messages one by one to avoid duplicate processing and handle errors with precision.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/posts\/3780","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/users\/209626"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/comments?post=3780"}],"version-history":[{"count":1,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/posts\/3780\/revisions"}],"predecessor-version":[{"id":3784,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/posts\/3780\/revisions\/3784"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/media\/3785"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/media?parent=3780"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/categories?post=3780"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/tags?post=3780"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}