Blog Post

Apps on Azure Blog
6 MIN READ

Take Control of Every Message: Partial Failure Handling for Service Bus Triggers in Azure Functions

swapnil_nagar's avatar
swapnil_nagar
Icon for Microsoft rankMicrosoft
Mar 23, 2026

Fine-grained failure handling for Service Bus batch triggers: Handle message-level failures in Azure Service Bus without reprocessing the entire batch.

The Problem: All-or-Nothing Batch Processing in Azure Service Bus

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.

But what happens when one message in the batch fails?

Your function receives a batch of 50 Service Bus messages. 49 process perfectly. 1 fails. What happens?

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:

  • Duplicate processing — messages that were already handled successfully get processed again
  • Wasted compute — you pay for re-executing work that already completed
  • Infinite retry loops — if that one "poison" message keeps failing, it blocks the entire batch indefinitely
  • Idempotency burden — your downstream systems must handle duplicates gracefully, adding complexity to every consumer

This is the classic all-or-nothing batch failure problem. Azure Functions solves it with per-message settlement.

The Solution: Per-Message Settlement for Azure Service Bus

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.

With Service Bus message settlement actions in Azure Functions, you can:

ActionWhat It Does
CompleteRemove the message from the queue (successfully processed)
AbandonRelease the lock so the message returns to the queue for retry, optionally modifying application properties
Dead-letterMove the message to the dead-letter queue (poison message handling)
DeferKeep the message in the queue but make it only retrievable by sequence number

This means in a batch of 50 messages, you can:

  • Complete 47 that processed successfully
  • Abandon 2 that hit a transient error (with updated retry metadata)
  • Dead-letter 1 that is malformed and will never succeed

All in a single function invocation. No reprocessing of successful messages. No building failure response objects. No all-or-nothing.

Why This Matters

1. Eliminates Duplicate Processing

When you complete messages individually, successfully processed messages are immediately removed from the queue. There's no chance of them being redelivered, even if other messages in the same batch fail.

2. Enables Granular Error Handling

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.

3. Implements Exponential Backoff Without External Infrastructure

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 additional queues or Durable Functions required.

4. Reduces Cost

You stop paying for redundant re-execution of already-successful work. In high-throughput systems processing millions of messages, this can be a material cost reduction.

5. Simplifies Idempotency Requirements

When successful messages are never redelivered, your downstream systems don't need to guard against duplicates as aggressively. This reduces architectural complexity and potential for bugs.

 

Before: One Message = One Function Invocation

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.

Single-Message Processing (The Old Way)

import { app, InvocationContext } from '@azure/functions';

async function processOrder(
    message: unknown,  // ← One message at a time, no batch
    context: InvocationContext
): Promise<void> {
    try {
        const order = message as Order;
        await processOrder(order);
    } catch (error) {
        context.error('Failed to process message:', error);
        // Message auto-complete by default.
        throw error;
    }
}

app.serviceBusQueue('processOrder', {
    connection: 'ServiceBusConnection',
    queueName: 'orders-queue',
    handler: processOrder,
});

 

What this cost you:

50 messages on the queueOld (single-message)New (batch + settlement)
Function invocations50 separate invocations1 invocation
Connection overhead50 separate DB/API connections1 connection, reused across batch
Compute cost50× invocation overhead1× invocation overhead
Settlement controlBinary: throw or don't4 actions per message

Every message paid the full price of a function invocation, startup, connection setup, teardown. At scale (millions of messages/day), this 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).

Code Examples

Let's see how this looks across all three major Azure Functions language stacks.

Node.js (TypeScript with @ azure/functions-extensions-servicebus)

import '@azure/functions-extensions-servicebus';
import { app, InvocationContext } from '@azure/functions';
import { ServiceBusMessageContext, messageBodyAsJson } from '@azure/functions-extensions-servicebus';

interface Order { id: string; product: string; amount: number; }

export async function processOrderBatch(
    sbContext: ServiceBusMessageContext,
    context: InvocationContext
): Promise<void> {
    const { messages, actions } = sbContext;

    for (const message of messages) {
        try {
            const order = messageBodyAsJson<Order>(message);
            await processOrder(order);
            await actions.complete(message);            // ✅ Done
        } catch (error) {
            context.error(`Failed ${message.messageId}:`, error);
            await actions.deadletter(message);          // ☠️ Poison
        }
    }
}

app.serviceBusQueue('processOrderBatch', {
    connection: 'ServiceBusConnection',
    queueName: 'orders-queue',
    sdkBinding: true,
    autoCompleteMessages: false,
    cardinality: 'many',
    handler: processOrderBatch,
});

Key points:

  • Enable sdkBinding: true and autoCompleteMessages: false to gain manual settlement control
  • ServiceBusMessageContext provides both the messages array and actions object
  • Settlement actions: complete(), abandon(), deadletter(), defer()
  • Application properties can be passed to abandon() for retry tracking
  • Built-in helpers like messageBodyAsJson<T>() handle Buffer-to-object parsing

Full sample: serviceBusSampleWithComplete

Python (V2 Programming Model)

import logging
from typing import List

import azure.functions as func
import azurefunctions.extensions.bindings.servicebus as servicebus

app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)


@app.service_bus_queue_trigger(arg_name="receivedmessage",
                               queue_name="QUEUE_NAME",
                               connection="SERVICEBUS_CONNECTION",
                               cardinality="many")
def servicebus_queue_trigger(receivedmessage: List[servicebus.ServiceBusReceivedMessage]):
    logging.info("Python ServiceBus queue trigger processed message.")
    for message in receivedmessage:
        logging.info("Receiving: %s\n"
                     "Body: %s\n"
                     "Enqueued time: %s\n"
                     "Lock Token: %s\n"
                     "Message ID: %s\n"
                     "Sequence number: %s\n",
                     message,
                     message.body,
                     message.enqueued_time_utc,
                     message.lock_token,
                     message.message_id,
                     message.sequence_number)


@app.service_bus_topic_trigger(arg_name="receivedmessage",
                               topic_name="TOPIC_NAME",
                               connection="SERVICEBUS_CONNECTION",
                               subscription_name="SUBSCRIPTION_NAME",
                               cardinality="many")
def servicebus_topic_trigger(receivedmessage: List[servicebus.ServiceBusReceivedMessage]):
    logging.info("Python ServiceBus topic trigger processed message.")
    for message in receivedmessage:
        logging.info("Receiving: %s\n"
                     "Body: %s\n"
                     "Enqueued time: %s\n"
                     "Lock Token: %s\n"
                     "Message ID: %s\n"
                     "Sequence number: %s\n",
                     message,
                     message.body,
                     message.enqueued_time_utc,
                     message.lock_token,
                     message.message_id,
                     message.sequence_number)

Key points:

  • Uses azurefunctions.extensions.bindings.servicebus for SDK-type bindings with ServiceBusReceivedMessage
  • Supports both queue and topic triggers with cardinality="many" for batch processing
  • Each message exposes SDK properties like body, enqueued_time_utc, lock_token, message_id, and sequence_number

Full sample: servicebus_samples_batch

.NET (C# Isolated Worker)

using Azure.Messaging.ServiceBus;
using Microsoft.Azure.Functions.Worker;

public class ServiceBusBatchProcessor(ILogger<ServiceBusBatchProcessor> logger)
{
    [Function(nameof(ProcessOrderBatch))]
    public async Task ProcessOrderBatch(
        [ServiceBusTrigger("orders-queue", Connection = "ServiceBusConnection")]
        ServiceBusReceivedMessage[] messages,
        ServiceBusMessageActions messageActions)
    {
        foreach (var message in messages)
        {
            try
            {
                var order = message.Body.ToObjectFromJson<Order>();
                await ProcessOrder(order);
                await messageActions.CompleteMessageAsync(message);       // ✅ Done
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Failed {MessageId}", message.MessageId);
                await messageActions.DeadLetterMessageAsync(message);     // ☠️ Poison
            }
        }
    }

    private Task ProcessOrder(Order order) => Task.CompletedTask;
}

public record Order(string Id, string Product, decimal Amount);

Key points:

  • Inject ServiceBusMessageActions directly alongside the message array
  • Each message is individually settled with CompleteMessageAsync, DeadLetterMessageAsync, or AbandonMessageAsync
  • Application properties can be modified on abandon to track retry metadata

Full sample: ServiceBusReceivedMessageFunctions.cs

How Azure Functions Per-Message Settlement Compares

Most serverless platforms offer some form of batch processing for message queues, but the level of control over individual message outcomes varies significantly. Here's how Azure Functions stacks up:

CapabilityAzure Functions (Service Bus)Typical Serverless Platform
Batch processing✅ Batch trigger (cardinality: many)✅ Supported
Partial failure handling✅ Per-message settlement⚠️ Binary report (succeeded/failed)
Dead-letter individual messages✅ deadletter() per message, in code❌ Relies on platform redrive policy
Abandon with modified properties✅ abandon() with property updates❌ Not typically supported
Defer messages✅ defer() per message❌ Not typically supported
Settlement granularity4 actions: complete, abandon, dead-letter, deferBinary: succeeded or failed
Retry metadata per messageBuilt into abandon via application propertiesMust manage externally (DB, cache)
Opt-in mechanismautoCompleteMessages: falseVaries by platform

Most platforms limit you to a binary outcome per message, succeeded or failed. Azure Functions gives you four distinct settlement actions, each with the ability to carry metadata, so your function logic can make nuanced decisions per message without external infrastructure.

Updated Mar 23, 2026
Version 2.0
No CommentsBe the first to comment