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:
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:
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:
- Validate the order.
- Reserve inventory.
- Process payment.
- Generate an invoice.
- Schedule delivery.
- 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.
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.
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:
- Validate the application.
- Check customer information.
- Verify documents.
- Request additional information.
- Wait for approval.
- Generate the agreement.
- 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:
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.
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:
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:
- Inventory is reserved.
- Payment is completed.
- 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:
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.
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.
Join the conversation! Your thoughts help the community grow.