---
title: "Building Reliable Background Processing with .NET Worker Services and Azure Service Bus"  
description: "How should a .NET worker handle queued Azure Service Bus messages, retries, duplicate delivery, and long-running work? Explore the design choices behind reliable background processing."  
author: "Joshua Williams"  
published: 2026-10-14  
updated: 2026-10-14  
canonical: https://answers.mindstick.com/blog/690/building-reliable-background-processing-with-net-worker-services-and-azure-service-bus  
category: "Azure with .NET Core"  
tags: ["azure", ".NET", "Azure Service Bus", "Worker Services", "Distributed Systems", "Background Jobs"]  
reading_time: 6 minutes  

---

# Building Reliable Background Processing with .NET Worker Services and Azure Service Bus

Web requests are a poor place to do work that might take several minutes, depend on another system, or need to be retried. A common Azure design is to put that work on an **Azure Service Bus queue** and handle it in a separate **[.NET Worker Service](https://answers.mindstick.com/blog/56/implementing-a-worker-service-for-newsletter-service-in-net)**. The web app can respond quickly, while the worker processes messages independently.

The queue does not make a job succeed by itself. You still need to decide what counts as success, what happens when processing fails, and how to cope when the same message is delivered more than once. Those details are what make a background-processing system dependable.

## Separate accepting work from doing work

A typical flow has three parts:

1. An ASP.NET Core API validates a request and sends a message to a queue.
2. A worker service receives the message and performs the longer-running task.
3. The worker settles the message after processing, or leaves it eligible for retry if processing fails.

For example, an API might accept an order and queue a request to generate an invoice. The API should not wait for the invoice to finish. It can return an accepted response with an order or job identifier, while a separate worker handles the invoice and records its status.

Keep the message focused on the work the consumer needs. It might contain an order ID and a requested operation rather than a large copy of the order and every related record. The worker can load current data from the system of record. This keeps messages smaller and reduces the chance that queued data becomes stale.

## Choose the right processing model

For a single consumer group handling queued work, a queue is usually the natural starting point. Use a topic when multiple independent subscribers need their own copy of each event—for instance, billing, analytics, and email services reacting to an order being placed.

In .NET, the Azure.Messaging.ServiceBus SDK provides a **ServiceBusProcessor** for receiving messages through callbacks. A processor can handle multiple messages concurrently, so the worker can make progress without manually polling the queue. Concurrency should be chosen with care: each message may use database connections, memory, or calls to other services. Increasing the number of concurrent handlers can overload those dependencies before it improves throughput.

## Settle messages only when the outcome is clear

With automatic completion disabled, the worker explicitly tells Service Bus what happened. Completing a message removes it from the queue. Abandoning it makes it available for delivery again. A message that repeatedly fails can eventually go to the **dead-letter queue**, depending on the entity's configured maximum delivery count.

This callback illustrates the basic choices. It assumes an `_orderHandler` is available to perform the business operation and an `_logger` is available for diagnostics.

```cs
private async Task HandleMessageAsync(ProcessMessageEventArgs args)
{
    Order? order;

    try
    {
        // Convert the queued JSON body into the message contract.
        order = args.Message.Body.ToObjectFromJson<Order>();
    }
    catch (JsonException ex)
    {
        // Invalid payloads will not improve with repeated processing attempts.
        _logger.LogWarning(ex, "Dead-lettering an order message with invalid JSON.");
        await args.DeadLetterMessageAsync(
            args.Message,
            "InvalidPayload",
            "The message body could not be read as an order.",
            args.CancellationToken);
        return;
    }

    if (order is null)
    {
        // Treat an empty payload as a permanent message problem.
        await args.DeadLetterMessageAsync(
            args.Message,
            "MissingPayload",
            "The message did not contain an order.",
            args.CancellationToken);
        return;
    }

    try
    {
        // Complete the message only after the business operation succeeds.
        await _orderHandler.ProcessAsync(order, args.CancellationToken);
        await args.CompleteMessageAsync(args.Message, args.CancellationToken);
    }
    catch (OperationCanceledException) when (args.CancellationToken.IsCancellationRequested)
    {
        // Let host shutdown or message cancellation proceed normally.
        throw;
    }
    catch (Exception ex)
    {
        // Log the failure and allow a later delivery to retry the operation.
        _logger.LogError(ex, "Order processing failed for {OrderId}.", order.Id);
        await args.AbandonMessageAsync(
            args.Message,
            cancellationToken: args.CancellationToken);
    }
}
```

In a real worker, make the distinction between a temporary failure and a bad message deliberately. A database timeout may be worth retrying; a payload that cannot be parsed usually is not. If a message is abandoned on every attempt, it will be delivered again until it succeeds or reaches the dead-letter queue. Monitor that queue and decide how operators should inspect, repair, or replay its messages.

## Design for duplicate delivery

Service Bus delivery is not the same as exactly-once business execution. A worker might finish updating a database and then lose its connection before it can complete the message. When the lock expires, Service Bus can deliver the message again. This is **duplicate message delivery**, and consumers should be designed with it in mind.

Make processing idempotent: handling the same message twice should not create two invoices, charge a customer twice, or apply the same state change twice. One practical approach is to assign each operation a stable identifier and record that identifier in the same database transaction as the business change. On a later delivery, the worker checks whether that operation has already been applied.

Do not rely on the queue's message identifier alone as a substitute for business-level idempotency. It can help identify a delivery, but the important question is whether the intended operation has already taken effect.

## Understand locks, retries, and long-running work

When a processor receives a message, Service Bus locks it for a limited time while the worker handles it. If processing outlasts the lock, the message can become available to another receiver. That can lead to two workers attempting the same task at once. Keep handlers bounded where possible, configure lock renewal appropriately for your workload, and ensure the underlying operation remains safe if it overlaps or repeats.

For work that may take a long time, consider splitting it into smaller messages or having the worker start a durable operation and record its progress. A single message should not become an untracked, hours-long task whose status is impossible to determine after a restart.

Retries also need limits and visibility. Repeatedly retrying a permanent failure only delays investigation. Use logs that include a correlation or operation ID, and watch delivery counts, processing failures, queue length, and the number of messages in the dead-letter queue. Those signals help distinguish a slow consumer from a broken dependency or a recurring bad payload.

## Run the worker as a separate Azure workload

A worker service can run as a container or as a hosted application in Azure. Keeping it separate from the API lets each workload scale according to its own pressure: the API can scale with incoming requests, while workers can scale with queue depth and processing time. Avoid scaling workers without checking downstream limits; more consumers can simply push a database or third-party service into failure.

Use managed identity for the worker's access to Azure resources where supported, and grant only the permissions it needs. Keep configuration such as the queue name and concurrency setting outside the compiled application so deployment environments can tune them independently. These operational choices do not replace careful message handling, but they make the worker easier to deploy, monitor, and adjust as demand changes.

---

Original Source: https://answers.mindstick.com/blog/690/building-reliable-background-processing-with-net-worker-services-and-azure-service-bus

Copyright © MindStick Software Pvt. Ltd. This Markdown version is provided for developers, AI systems, and offline reading.
