Service Bus Integration with Azure Functions

Azure Functions integrates natively with Azure Service Bus through Service Bus Triggers and Service Bus Output Bindings. A Service Bus Trigger automatically invokes a function whenever a message arrives in a queue or topic subscription — no polling code required. An Output Binding lets a function send messages to Service Bus as a simple return value or output object, without creating a sender client manually. This combination makes Azure Functions the most efficient way to build event-driven, serverless message processors on top of Service Bus.

Integration Architecture

TRIGGER — Function invoked when message arrives:

[Service Bus Queue / Subscription]
       |
       | message arrives
       v
[Azure Function - ServiceBusTrigger]
       |
       | processes message
       v
[Business logic + downstream services]


OUTPUT BINDING — Function sends message to Service Bus:

[HTTP Trigger / Timer / Event Grid]
       |
       v
[Azure Function]
       |
       | return value / IAsyncCollector
       v
[Service Bus Queue / Topic]

SDK and Tool Setup

# Install Azure Functions Core Tools
npm install -g azure-functions-core-tools@4

# Create a new function app (.NET isolated worker)
func init OrderProcessingApp --worker-runtime dotnet-isolated

# Navigate to app folder
cd OrderProcessingApp

# Add Service Bus extension
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.ServiceBus
dotnet add package Azure.Identity

1. Service Bus Trigger — Queue

The [ServiceBusTrigger] attribute connects a function to a queue. The function runs each time a message arrives. Azure Functions manages the receiver, lock, and completion automatically.

using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;

public class OrderProcessor
{
    private readonly ILogger<OrderProcessor> _logger;

    public OrderProcessor(ILogger<OrderProcessor> logger)
    {
        _logger = logger;
    }

    [Function("ProcessOrder")]
    public void Run(
        [ServiceBusTrigger("orders", Connection = "ServiceBusConnection")]
        ServiceBusReceivedMessage message,
        ServiceBusMessageActions messageActions)
    {
        _logger.LogInformation("Order received: {MessageId}", message.MessageId);
        _logger.LogInformation("Body: {Body}", message.Body.ToString());
        _logger.LogInformation("DeliveryCount: {Count}", message.DeliveryCount);

        // Read custom properties
        if (message.ApplicationProperties.TryGetValue("Region", out var region))
            _logger.LogInformation("Region: {Region}", region);

        // Auto-complete is on by default — message deleted after function returns
        // To manually control settlement, set AutoCompleteMessages = false in host.json
    }
}

Connection String Configuration

// local.settings.json (development)
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage"   : "UseDevelopmentStorage=true",
    "ServiceBusConnection"  : "Endpoint=sb://myshopns.servicebus.windows.net/;SharedAccessKeyName=...;SharedAccessKey=...;",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"
  }
}

// For production in Azure App Settings:
//   ServiceBusConnection = "Endpoint=sb://myshopns.servicebus.windows.net/;..."
//
// For Managed Identity (no connection string):
//   ServiceBusConnection__fullyQualifiedNamespace = "myshopns.servicebus.windows.net"

2. Service Bus Trigger — Topic Subscription

[Function("ProcessInventoryEvent")]
public void Run(
    [ServiceBusTrigger(
        "order-events",             // topic name
        "inventory-sub",            // subscription name
        Connection = "ServiceBusConnection")]
    ServiceBusReceivedMessage message,
    ILogger log)
{
    log.LogInformation($"Inventory event: {message.Body}");
    log.LogInformation($"Subject: {message.Subject}");
}

3. Manual Message Settlement

By default, the function runtime completes the message automatically when the function returns without error, and abandons it when the function throws an exception. For full manual control, disable auto-complete in host.json and use ServiceBusMessageActions.

Disable AutoComplete in host.json

{
  "version"      : "2.0",
  "extensions"   : {
    "serviceBus" : {
      "prefetchCount"         : 0,
      "messageHandlerOptions" : {
        "autoComplete"        : false,
        "maxConcurrentCalls"  : 10,
        "maxAutoRenewDuration": "00:05:00"
      }
    }
  }
}

Manual Settlement in Function Code

[Function("ManualSettlementExample")]
public async Task Run(
    [ServiceBusTrigger("orders", Connection = "ServiceBusConnection")]
    ServiceBusReceivedMessage message,
    ServiceBusMessageActions messageActions,
    ILogger log)
{
    string body = message.Body.ToString();

    try
    {
        if (!body.Contains("orderId"))
        {
            // Invalid message — dead-letter it immediately
            await messageActions.DeadLetterMessageAsync(
                message,
                deadLetterReason: "SchemaValidationFailed",
                deadLetterErrorDescription: "Missing required field: orderId"
            );
            log.LogWarning("Message dead-lettered: invalid schema.");
            return;
        }

        // Process the order...
        log.LogInformation($"Processing order: {body}");

        await messageActions.CompleteMessageAsync(message);
        log.LogInformation("Message completed.");
    }
    catch (Exception ex)
    {
        log.LogError($"Processing failed: {ex.Message}. Abandoning.");
        await messageActions.AbandonMessageAsync(message);
    }
}

4. Output Binding — Send Message to Queue

The [ServiceBus] output binding sends a message to a queue or topic as a return value — no sender client code needed.

HTTP Trigger → Send to Queue

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

public class OrderIngestion
{
    [Function("SubmitOrder")]
    [ServiceBusOutput("orders", Connection = "ServiceBusConnection")]
    public ServiceBusMessage Run(
        [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req,
        ILogger log)
    {
        string body = new StreamReader(req.Body).ReadToEnd();
        log.LogInformation($"Order received from HTTP: {body}");

        var message = new ServiceBusMessage(body)
        {
            MessageId   = Guid.NewGuid().ToString(),
            Subject     = "NewOrder",
            ContentType = "application/json"
        };
        message.ApplicationProperties["Source"] = "HTTP";

        return message; // Azure Functions sends this to "orders" queue automatically
    }
}

Timer Trigger → Send Batch to Queue

[Function("DailyBatchDispatch")]
public async Task Run(
    [TimerTrigger("0 0 9 * * *")] TimerInfo timer,  // Every day at 9 AM
    [ServiceBus("orders", Connection = "ServiceBusConnection")]
    IAsyncCollector<ServiceBusMessage> outputQueue,
    ILogger log)
{
    var pendingOrders = GetPendingOrdersFromDatabase(); // your data source

    foreach (var order in pendingOrders)
    {
        var msg = new ServiceBusMessage(System.Text.Json.JsonSerializer.Serialize(order))
        {
            MessageId   = $"batch-{order.Id}",
            ContentType = "application/json"
        };
        await outputQueue.AddAsync(msg);
        log.LogInformation($"Queued order {order.Id}");
    }

    log.LogInformation($"Batch dispatch complete. {pendingOrders.Count} orders queued.");
}

5. Using Managed Identity (No Connection String)

// In Azure Function App Settings, set:
//   ServiceBusConnection__fullyQualifiedNamespace = myshopns.servicebus.windows.net

// Assign the Function App's Managed Identity these roles on the namespace:
//   Azure Service Bus Data Receiver  (for trigger)
//   Azure Service Bus Data Sender    (for output binding)

// No connection string in settings — no secrets to rotate.

6. Concurrency and Scale Configuration

SettingLocationPurpose
maxConcurrentCallshost.jsonMax parallel messages per function instance
maxAutoRenewDurationhost.jsonHow long to auto-renew message locks
prefetchCounthost.jsonMessages to pre-fetch for performance
FUNCTIONS_WORKER_PROCESS_COUNTApp SettingsWorker processes per instance
WEBSITE_MAX_DYNAMIC_APPLICATION_SCALE_OUTApp SettingsMax scale-out instances (Consumption plan)
{
  "version": "2.0",
  "extensions": {
    "serviceBus": {
      "prefetchCount": 10,
      "messageHandlerOptions": {
        "maxConcurrentCalls"  : 16,
        "maxAutoRenewDuration": "00:10:00",
        "autoComplete"        : true
      }
    }
  }
}

7. Scale Behavior on Consumption Plan

Queue message count: 0
  --> Function: 0 instances (scale to zero)

Queue message count: 1,000
  --> Function: scales out automatically (up to max instances)

Queue message count: 10,000
  --> Function: at max instances (e.g., 200) all processing in parallel

Queue empties: 0 messages
  --> Function: scales back to 0 (Consumption plan)

8. Error Handling and Retry Policy

// host.json — configure retry policy for failed function executions
{
  "version": "2.0",
  "retry": {
    "strategy"        : "exponentialBackoff",
    "maxRetryCount"   : 5,
    "minimumInterval" : "00:00:05",
    "maximumInterval" : "00:02:00"
  }
}

// If all retries fail:
//   Message DeliveryCount exceeds MaxDeliveryCount on the queue
//   --> Message moves to Dead Letter Queue automatically

Trigger + Output Binding — Chained Processing Pattern

[Function("ValidateAndRoute")]
[ServiceBusOutput("validated-orders", Connection = "ServiceBusConnection")]
public ServiceBusMessage? Run(
    [ServiceBusTrigger("raw-orders", Connection = "ServiceBusConnection")]
    ServiceBusReceivedMessage message,
    ServiceBusMessageActions actions,
    ILogger log)
{
    string body = message.Body.ToString();

    if (!IsValid(body))
    {
        actions.DeadLetterMessageAsync(message, "ValidationFailed", "Schema mismatch");
        return null; // Don't forward invalid messages
    }

    // Forward valid message to next queue in pipeline
    return new ServiceBusMessage(body)
    {
        MessageId = message.MessageId,
        Subject   = "ValidatedOrder"
    };
}

// Pipeline:
// [raw-orders] --> Function(ValidateAndRoute) --> [validated-orders]

Summary

Azure Functions integrates with Service Bus through Triggers (automatic invocation on message arrival) and Output Bindings (sending messages without client code). Triggers work for queues and topic subscriptions, support manual message settlement via ServiceBusMessageActions, and scale automatically from zero to hundreds of instances based on queue depth. Output bindings let any function type — HTTP, Timer, Event Grid — produce messages to Service Bus queues or topics with a simple return value or IAsyncCollector. Using Managed Identity with fullyQualifiedNamespace app settings removes all connection string secrets. Azure Functions and Service Bus together form the foundation of scalable, serverless, event-driven architectures on Azure.

Leave a Comment