How to Implement Background Jobs in ASP.NET Core 10 Using Hangfire and SQL Server (2026)

Hangfire stores background jobs in SQL Server so an ASP.NET Core request can return after enqueuing work. A worker later loads the job and executes it. Treat a job as potentially repeatable. If a worker records a result and fails before marking the job complete, another worker can run it again.

This example uses a database receipt keyed by order ID to make that repeat safe. The job does not send email or call a payment system. Those side effects need their own idempotency boundary.

Architecture and prerequisites

In the reference build, explicitly reference System.Security.Cryptography.Pkcs 10.0.14 to avoid vulnerable transitive versions. Keep NuGet auditing enabled and update the pin as patched releases become available.

The API enqueues a job in SQL Server. A Hangfire worker executes it and the application database deduplicates the receipt.
The API enqueues a job in SQL Server. A Hangfire worker executes it and the application database deduplicates the receipt.

Install .NET 10 and start a SQL Server instance you control. Create a database called JobsDemo. Create the application table with a migration or this SQL. Hangfire manages its own storage schema separately.

SQL
CREATE TABLE dbo.JobReceipt (
    OrderId int NOT NULL CONSTRAINT PK_JobReceipt PRIMARY KEY,
    CreatedUtc datetime2 NOT NULL
        CONSTRAINT DF_JobReceipt_CreatedUtc DEFAULT SYSUTCDATETIME()
);

Create a web project and add Hangfire.AspNetCore, Hangfire.SqlServer and Microsoft.Data.SqlClient. Keep the two Hangfire package versions aligned and explicitly reference Newtonsoft.Json 13.0.4 to avoid the vulnerable version resolved by the default dependency graph. Set ConnectionStrings__Jobs through user secrets or protected process configuration. The lab connection needs access to the application table and permission to initialize Hangfire’s schema. Pre-provision the schema and restrict the worker login in production. Keep schema migrations separate from routine job execution.

Hangfire ASP.NET Core setup · SQL Server storage

Implement the job and endpoints

Replace Program.cs in a minimal web project. The entire demo assumes direct loopback access and must never sit behind a reverse proxy. A local address is not a trusted authorization boundary behind a proxy. For deployment, remove the lab guard only after replacing it with authenticated endpoint policies and a custom dashboard authorization filter.

Program.cs
using Hangfire;
using Hangfire.Dashboard;
using Hangfire.SqlServer;
using Microsoft.Data.SqlClient;
using System.Net;

var b = WebApplication.CreateBuilder(args);
var sql = b.Configuration.GetConnectionString("Jobs")
    ?? throw new InvalidOperationException("Set ConnectionStrings:Jobs.");
b.Services.AddHangfire(c => c.UseSqlServerStorage(sql));
b.Services.AddHangfireServer();
b.Services.AddTransient<ReceiptJob>();
var app = b.Build();
app.Use(async (context, next) =>
{
    var remote = context.Connection.RemoteIpAddress;
    if (remote is null || !IPAddress.IsLoopback(remote))
    {
        context.Response.StatusCode = 403;
        return;
    }
    await next();
});
app.UseHangfireDashboard("/hangfire");

app.MapPost("/jobs/receipts/{orderId:int:min(1)}",
    (int orderId, IBackgroundJobClient jobs) =>
{
    var id = jobs.Enqueue<ReceiptJob>(
        j => j.RecordAsync(orderId, CancellationToken.None));
    return Results.Accepted(value: new { JobId = id });
});

app.MapPost("/jobs/reminders/{orderId:int:min(1)}",
    (int orderId, IBackgroundJobClient jobs) =>
{
    var id = jobs.Schedule<ReceiptJob>(
        j => j.RecordAsync(orderId, CancellationToken.None), TimeSpan.FromMinutes(2));
    return Results.Accepted(value: new { JobId = id });
});

app.Services.GetRequiredService<IRecurringJobManager>().AddOrUpdate<ReceiptJob>(
    "daily-demo-receipt", j => j.RecordAsync(999, CancellationToken.None), Cron.Daily());
app.Run();

public sealed class ReceiptJob(IConfiguration configuration)
{
    [AutomaticRetry(Attempts = 3)]
    public async Task RecordAsync(int orderId, CancellationToken cancellationToken)
    {
        await using var db = new SqlConnection(configuration.GetConnectionString("Jobs"));
        await db.OpenAsync(cancellationToken);
        using var command = db.CreateCommand();
        command.CommandText = """
            INSERT INTO dbo.JobReceipt (OrderId) VALUES (@orderId);
            """;
        command.Parameters.Add("@orderId", System.Data.SqlDbType.Int).Value = orderId;
        try
        {
            await command.ExecuteNonQueryAsync(cancellationToken);
        }
        catch (SqlException e) when (e.Number is 2601 or 2627)
        {
            // This table has one unique key: the order ID.
        }
    }
}

The queue stores a serialized method call and its arguments. Pass stable identifiers instead of large objects, personal records or secrets. The worker should reload the current state as needed. The sample registers a worker in the API process. A separate worker process is useful when job execution and HTTP handling need independent scaling or deployments.

The primary key protects the receipt even when two workers attempt the insert concurrently. The code handles the duplicate constraint as an already-completed operation. Keep that catch scoped to the intended insert. If the table gains other unique constraints, distinguish the expected conflict explicitly.

Verify the behavior

Start the application directly on loopback. POST twice to /jobs/receipts/101. Expect two HTTP 202 responses with different Hangfire job IDs. Open /hangfire locally and follow both jobs. Once both have run, query the table. Expect one row for order 101.

SQL
SELECT OrderId, CreatedUtc
FROM dbo.JobReceipt
WHERE OrderId IN (101, 102, 999);

POST to /jobs/reminders/102. The job should appear as scheduled before processing. The two-minute delay is the earliest eligible time. Polling and queue load can make execution later. The daily job uses a fixed demonstration order and a stable recurring-job ID. It deliberately shows how a schedule can repeat while the business receipt remains unique. These are expected results. SQL Server execution was not verified here.

Job Trigger Operational consideration
Fire-and-forget Enqueued immediately A successful response confirms enqueueing.
Delayed Becomes eligible after a delay Keep a worker running when due.
Recurring Schedule produces a job Confirm timezone and monitor missed or late executions.

Retry and monitoring decisions

An exception can trigger a retry. This sample configures three retries for the job. An unrecoverable error should be visible to operators after retries are exhausted. Track queue age, processing time, failed jobs and retry volume. Queue length alone can be misleading if a few expensive jobs occupy all workers.

For a real external notification, use a stable event ID, an outbox record and a provider-side deduplication key where available. Enqueuing a job and separately committing a business change creates a gap between two writes. See the ASP.NET PB transactional outbox guide for the database transaction boundary.

Troubleshooting and FAQs

The dashboard loads but jobs never run. Check that a Hangfire server is registered and appears online. Verify that the API and worker connect to the same database and queues.

Why was my job invoked twice? A retry, duplicate enqueue or worker interruption can cause repeated execution. Deduplicate the business effect rather than relying on the number of invocations.

Can I expose the dashboard to all signed-in users? Restrict it to an operations policy. Job arguments, error details and controls can be sensitive. Require HTTPS and verify authorization at the dashboard itself.

How do I deploy safely while jobs are queued? Keep serialized method signatures compatible with queued work, or drain and migrate it deliberately. Tune worker concurrency according to downstream limits.

A useful release check is to force the same business job twice and inspect the durable result. Queue persistence lets the work resume. The database constraint makes repetition harmless for the specific operation shown here.

Hangfire recurring jobs · Dashboard authorization · Exception handling

Comments