Many applications need to perform work outside the normal HTTP request and response cycle.

For example, an e-commerce application may send order confirmation emails, process payments, generate invoices, or synchronize inventory after an order is placed.

A common approach is to use .NET Background Services. They allow applications to execute long-running tasks without keeping an HTTP request open.

However, some business operations are more complicated than a simple background task. They may involve multiple steps, retries, external APIs, approval requirements, and recovery after application restarts.

In these situations, a durable workflow may be a better choice.

This article explains the differences between .NET Background Services and durable workflows, when to use each approach, and how to choose the right option for production applications.

Understand .NET Background Services

A .NET Background Service is a hosted service that runs alongside an ASP.NET Core application.

It is useful for operations that need to execute continuously or periodically without blocking incoming HTTP requests.

Common examples include:

  • Processing queued messages.
  • Sending scheduled notifications.
  • Cleaning temporary files.
  • Synchronizing data.
  • Checking pending database records.
  • Running periodic maintenance tasks.

.NET provides the BackgroundService base class for implementing this behavior.

For example, create a worker that checks for pending orders:

C#
public class OrderWorker : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<OrderWorker> _logger;

    public OrderWorker(
        IServiceScopeFactory scopeFactory,
        ILogger<OrderWorker> logger)
    {
        _scopeFactory = scopeFactory;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(
        CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                using var scope = _scopeFactory
                    .CreateScope();

                var processor = scope.ServiceProvider
                    .GetRequiredService<IOrderProcessor>();

                await processor.ProcessPendingOrdersAsync(
                    stoppingToken);
            }
            catch (OperationCanceledException)
                when (stoppingToken.IsCancellationRequested)
            {
                break;
            }
            catch (Exception ex)
            {
                _logger.LogError(
                    ex,
                    "Order processing failed.");
            }

            await Task.Delay(
                TimeSpan.FromSeconds(10),
                stoppingToken);
        }
    }
}

Register the worker in Program.cs:

C#
builder.Services.AddHostedService<OrderWorker>();

The worker runs when the application starts and stops when the host shuts down.

The example creates a dependency injection scope for each processing cycle. This is important when the processor uses scoped services such as Entity Framework Core's DbContext.

Understand Durable Workflows

A durable workflow is designed to preserve execution progress even when the application or worker stops unexpectedly.

Instead of keeping the entire operation in memory, the workflow engine or application persists its execution state.

Consider an order fulfillment process:

  1. Validate the order.
  2. Reserve inventory.
  3. Process payment.
  4. Generate an invoice.
  5. Schedule delivery.
  6. Send confirmation.

If payment processing fails after inventory has been reserved, the application needs to know which steps succeeded and what should happen next.

A durable workflow records the execution state of individual steps.

Code
Order Created
      |
      v
Validate Order
      |
      v
Reserve Inventory
      |
      v
Process Payment
      |
      | Failure
      v
Schedule Retry
      |
      v
Resume Workflow
      |
      v
Generate Invoice
      |
      v
Schedule Delivery

The workflow can continue from its saved state instead of repeating every operation.

Durable workflows are useful for business processes that may run for minutes, hours, or days.

Compare Background Services and Durable Workflows

Both approaches execute work outside the normal request pipeline, but they solve different problems.

Feature

Background Service

Durable Workflow

Execution

Runs inside the application host

Managed by a workflow engine or durable execution layer

State persistence

Must be implemented separately

Execution state is persisted

Application restart

Work may stop unless recovered

Can resume from persisted state

Retry handling

Implemented by the application

Often provides workflow-level retry support

Multiple steps

Managed through application code

Represented as workflow steps

Long-running tasks

Possible with additional design

Designed for durable execution

Compensation

Must be implemented

Can be modeled as part of the workflow

Operational complexity

Lower for simple tasks

Higher initial setup

Best use case

Polling, cleanup, queue processing

Multi-step business processes

A Background Service does not automatically provide durable execution.

If the application stops while a task is running, the task may be interrupted. You need a database, queue, or another persistence mechanism to recover it.

Similarly, a durable workflow does not eliminate the need for careful application design. External operations still need idempotency, error handling, and appropriate authorization.

When Should You Use a Background Service?

Use a Background Service when the operation is relatively simple and does not require complex execution history.

For example, an application may need to remove expired temporary files every hour.

C#
public class CleanupWorker : BackgroundService
{
    private readonly ILogger<CleanupWorker> _logger;

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

    protected override async Task ExecuteAsync(
        CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await DeleteExpiredFilesAsync(
                    stoppingToken);
            }
            catch (OperationCanceledException)
                when (stoppingToken.IsCancellationRequested)
            {
                break;
            }
            catch (Exception ex)
            {
                _logger.LogError(
                    ex,
                    "File cleanup failed.");
            }

            await Task.Delay(
                TimeSpan.FromHours(1),
                stoppingToken);
        }
    }

    private Task DeleteExpiredFilesAsync(
        CancellationToken cancellationToken)
    {
        // Delete expired files
        return Task.CompletedTask;
    }
}

This is a good use case because the operation can be repeated during the next scheduled cycle.

Other suitable examples include:

  • Refreshing a small cache.
  • Checking scheduled reminders.
  • Processing a simple queue.
  • Sending noncritical notifications.
  • Performing routine maintenance.

If a worker needs to process messages reliably, use a durable queue or database-backed job store rather than relying only on an in-memory loop.

When Should You Use a Durable Workflow?

Use a durable workflow when business operations contain multiple dependent steps and must survive interruptions.

Consider a loan application system.

The process may involve:

  1. Validate the application.
  2. Check customer information.
  3. Verify documents.
  4. Request additional information.
  5. Wait for approval.
  6. Generate the agreement.
  7. Notify the customer.

Some steps may complete immediately, while others may wait for hours or days.

A normal Background Service can execute this process, but you would need to implement persistence, state transitions, retries, timeouts, and recovery yourself.

A durable workflow provides a more structured way to manage these requirements.

It is particularly useful when the application needs:

  • Long-running business processes.
  • Human approval steps.
  • Scheduled retries.
  • Timeout handling.
  • Recovery after restarts.
  • Multiple external integrations.
  • Compensation for completed operations.

Implement a Simple Background Job With Persistence

Before introducing a workflow engine, consider whether a database-backed job is enough.

Create a job entity:

C#
public class BackgroundJob
{
    public Guid Id { get; set; }

    public string JobType { get; set; }
        = string.Empty;

    public string Status { get; set; }
        = "Pending";

    public int RetryCount { get; set; }

    public DateTime CreatedAtUtc { get; set; }

    public DateTime? CompletedAtUtc { get; set; }

    public DateTime? NextRetryAtUtc { get; set; }
}

A worker can retrieve pending jobs, process them, and update their status.

C#
public async Task ProcessJobAsync(
    BackgroundJob job,
    CancellationToken cancellationToken)
{
    try
    {
        job.Status = "Running";

        await _context.SaveChangesAsync(
            cancellationToken);

        await _jobHandler.ExecuteAsync(
            job.JobType,
            cancellationToken);

        job.Status = "Completed";
        job.CompletedAtUtc = DateTime.UtcNow;
    }
    catch (Exception)
    {
        job.RetryCount++;

        job.Status = job.RetryCount >= 5
            ? "Failed"
            : "Pending";

        job.NextRetryAtUtc = DateTime.UtcNow
            .AddMinutes(job.RetryCount * 2);
    }

    await _context.SaveChangesAsync(
        cancellationToken);
}

This example demonstrates basic job persistence.

For production use, add atomic job claiming, concurrency control, error classification, retry limits, and a lease or recovery mechanism for jobs left in the running state.

You should also avoid storing executable code or arbitrary type names in the database. Map approved job types to registered handlers.

A database-backed job system can be enough for many applications without introducing a full workflow engine.

Consider a Durable Workflow Engine

When application-managed job state becomes difficult to maintain, a dedicated workflow solution may be useful.

For example, <Entity value="Temporal" category="software" disambig="Durable workflow orchestration platform"/> provides durable workflow execution with persisted state, retries, timers, and recovery.

<AsyncImage query="Temporal workflow orchestration architecture diagram workers workflow history" aspectRatio="16:9" width="100%" />

A workflow can describe the order fulfillment process:

SQL
OrderWorkflow
    |
    +-- ValidateOrder
    |
    +-- ReserveInventory
    |
    +-- ProcessPayment
    |
    +-- GenerateInvoice
    |
    +-- ScheduleDelivery
    |
    +-- SendConfirmation

The workflow engine manages execution history and coordinates the workflow's progress.

Other options include <Entity value="Azure Durable Functions" category="software" disambig="Azure serverless workflow orchestration service"/> for applications built on Azure Functions, and <Entity value="Dapr Workflow" category="software" disambig="Distributed workflow orchestration framework"/> for distributed applications using Dapr.

The best choice depends on hosting infrastructure, operational requirements, language support, and the team's experience.

Understand Retries and Compensation

Retries are important in both background services and durable workflows.

However, retrying every failed operation can create business problems.

For example, consider an order process:

  1. Inventory is reserved.
  2. Payment is completed.
  3. Invoice generation fails.

Repeating the entire workflow could reserve inventory again or charge the customer twice.

Instead, retry only the failed operation when it is safe.

For operations that cannot simply be repeated, use idempotency keys.

For example:

C#
public async Task ProcessPaymentAsync(
    Guid orderId,
    CancellationToken cancellationToken)
{
    var idempotencyKey =
        $"order-payment-{orderId}";

    await _paymentService.ChargeAsync(
        orderId,
        idempotencyKey,
        cancellationToken);
}

The payment provider must support and enforce the idempotency key.

Compensation is another important concept.

If inventory has been reserved but payment permanently fails, the application may need to release the reservation.

Code
Reserve Inventory
       |
       v
Process Payment
       |
       | Permanent Failure
       v
Release Inventory
       |
       v
Mark Order Failed

A compensation operation does not necessarily reverse the original action perfectly. It performs a new business operation that restores an acceptable state.

This is why complex workflows often benefit from an explicit orchestration model.

Deployment and Scaling Considerations

A Background Service runs inside the application process.

If you deploy multiple ASP.NET Core instances, each instance may start its own copy of the worker.

This can result in duplicate processing if the application does not use distributed coordination.

For example, three application instances may all attempt to process the same pending database job.

Use database leases, atomic claims, or a distributed queue to coordinate workers.

Durable workflow engines generally provide their own execution coordination, but their workers and infrastructure still need to be deployed and monitored correctly.

Also consider hosting environments that scale application instances down when there is no incoming traffic. A background workload should not depend on an instance remaining active unless the hosting configuration guarantees that behavior.

For important workloads, consider a separate worker deployment rather than combining all background processing with the public API.

Best Practices for Choosing the Right Approach

Start with the simplest design

Do not introduce a workflow engine for a task that only needs a periodic database check.

Persist important job state

If losing a task during application shutdown would cause a business problem, do not keep its only state in memory.

Use idempotent operations

Retries and duplicate delivery are normal in distributed systems. Make business operations safe to repeat where possible.

Separate API and worker responsibilities

An API should accept requests quickly, while background components handle lengthy operations.

Monitor execution history

Track job status, execution duration, retry count, and failure reasons.

Use workflow orchestration when complexity justifies it

Multiple dependent steps, long waits, approvals, compensation, and recovery are strong reasons to consider a durable workflow.

Summary

.NET Background Services and durable workflows both help applications execute work outside the HTTP request lifecycle, but they solve different problems.

Background Services are a good choice for polling, cleanup, simple queue processing, and recurring maintenance tasks.

Durable workflows are better suited to long-running business operations that require persisted execution state, retries, recovery, timeouts, and compensation.

For simpler applications, a database-backed job system may provide enough reliability without introducing a separate workflow engine.

Choose the approach based on how important the work is, how long it runs, and what should happen if the application stops halfway through.