NexJob 0.3.2
See the version list below for details.
dotnet add package NexJob --version 0.3.2
NuGet\Install-Package NexJob -Version 0.3.2
<PackageReference Include="NexJob" Version="0.3.2" />
<PackageVersion Include="NexJob" Version="0.3.2" />
<PackageReference Include="NexJob" />
paket add NexJob --version 0.3.2
#r "nuget: NexJob, 0.3.2"
#:package NexJob@0.3.2
#addin nuget:?package=NexJob&version=0.3.2
#tool nuget:?package=NexJob&version=0.3.2
<div align="center">
<br/>
███╗ ██╗███████╗██╗ ██╗ ██╗ ██████╗ ██████╗
████╗ ██║██╔════╝╚██╗██╔╝ ██║██╔═══██╗██╔══██╗
██╔██╗ ██║█████╗ ╚███╔╝ ██║██║ ██║██████╔╝
██║╚██╗██║██╔══╝ ██╔██╗ ██ ██║██║ ██║██╔══██╗
██║ ╚████║███████╗██╔╝ ██╗╚█████╔╝╚██████╔╝██████╔╝
╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝ ╚════╝ ╚═════╝ ╚═════╝
Background jobs for .NET that stay out of your way.
<br/>
</div>
NexJob is the background job library that .NET deserved from day one.
No expression trees. No lock-in. No paid tiers for Redis. No wrestling with serialization. Just a clean interface, two lines of configuration, and a scheduler that handles the hard parts — retries, concurrency, cron, observability — while you focus on what your job actually does.
public class WelcomeEmailJob : IJob<WelcomeEmailInput>
{
public async Task ExecuteAsync(WelcomeEmailInput input, CancellationToken ct)
=> await _email.SendAsync(input.UserId, ct);
}
That's your entire job. NexJob handles the rest.
Why NexJob exists
Every .NET developer has used Hangfire. And every .NET developer has hit the same walls:
- The Redis adapter costs money. So does the MongoDB one.
async/awaitis bolted on — Hangfire serializesTask, it doesn't await it.- The dashboard looks like it was designed in 2014. Because it was.
- There's no concept of priority queues, resource throttling, or payload versioning.
- You can't change workers or pause a queue without restarting the server.
- Schema migrations don't exist — add a column and deployments break.
- The license is LGPL for the core and paid for anything production-worthy.
NexJob was built to solve all of that.
At a glance
| NexJob | Hangfire | |
|---|---|---|
| License | MIT | LGPL / paid Pro |
async/await native |
✅ | ❌ |
| Priority queues | ✅ | ❌ |
Resource throttling ([Throttle]) |
✅ | ❌ |
Per-job retry config ([Retry]) |
✅ | ❌ |
| Idempotency keys | ✅ | ❌ |
| Job continuations (chaining) | ✅ | ❌ |
appsettings.json support |
✅ | ❌ |
| Execution windows per queue | ✅ | ❌ |
| Live config without restart | ✅ | ❌ |
| Schema migrations (auto) | ✅ | ❌ |
| Graceful shutdown | ✅ | ❌ |
| Distributed recurring lock | ✅ | ❌ |
| OpenTelemetry built-in | ✅ | ❌ |
Payload versioning (IJobMigration) |
✅ | ❌ |
| Job progress tracking | ✅ | ✅ paid |
Job context (IJobContext) |
✅ | ✅ |
| Job tags | ✅ | ❌ |
| All storage adapters free | ✅ | ❌ |
| In-memory for testing | ✅ | ✅ |
| Cron / recurring jobs | ✅ | ✅ |
| Dashboard | ✅ dark mode | ✅ legacy |
| Worker Service / Console dashboard | ✅ | ❌ |
Job progress tracking,IJobContext, andJob tagsare new in v0.3.
Benchmarks
Measured on Intel Xeon E5-2667 v4 3.20GHz, 16 logical cores, .NET 8.0.25, March 2026. Run
dotnet run -c Release --project benchmarks/NexJob.Benchmarks -- --filter '*' --job Shortto reproduce. Full raw results in benchmarks/results/README.md.
Enqueue latency — single job, in-memory storage
| NexJob | Hangfire | Difference | |
|---|---|---|---|
| Mean latency | 9.28 μs | 26.63 μs | NexJob is 2.87x faster |
| Allocated memory | 1.67 KB | 11.2 KB | NexJob uses 85% less memory |
Installation
# Core (includes in-memory provider for dev/tests)
dotnet add package NexJob
# Pick your storage — all free, all open-source
dotnet add package NexJob.Postgres
dotnet add package NexJob.SqlServer
dotnet add package NexJob.Redis
dotnet add package NexJob.MongoDB
dotnet add package NexJob.Oracle
# For Web APIs
dotnet add package NexJob.Dashboard
# For Worker Services and Console Apps
dotnet add package NexJob.Dashboard.Standalone
# Or scaffold a complete starter project
dotnet new install NexJob.Templates
dotnet new nexjob -n MyApp
Worker Services & Console Apps
NexJob works in any .NET host — not just Web APIs.
For Worker Services and Console Applications that don't have an HTTP pipeline,
install NexJob.Dashboard.Standalone to get the full dashboard without any extra
project or infrastructure.
dotnet add package NexJob.Dashboard.Standalone
// Worker Service — Program.cs
var builder = Host.CreateApplicationBuilder(args);
builder.Services
.AddNexJob(builder.Configuration)
.AddNexJobJobs(typeof(Program).Assembly);
builder.Services.AddNexJobStandaloneDashboard(builder.Configuration);
await builder.Build().RunAsync();
// Dashboard: http://localhost:5005/dashboard
Configure the port and path in appsettings.json:
{
"NexJob": {
"Dashboard": {
"Port": 5005,
"Path": "/dashboard",
"Title": "My Worker Jobs",
"LocalhostOnly": true
}
}
}
| Scenario | Package | Registration |
|---|---|---|
| Web API / ASP.NET Core | NexJob.Dashboard |
app.UseNexJobDashboard() |
| Worker Service / Console App | NexJob.Dashboard.Standalone |
services.AddNexJobStandaloneDashboard() |
Getting started
1 — Define your job
public record SendInvoiceInput(Guid OrderId, string CustomerEmail);
public class SendInvoiceJob : IJob<SendInvoiceInput>
{
private readonly IInvoiceService _invoices;
private readonly IEmailService _email;
public SendInvoiceJob(IInvoiceService invoices, IEmailService email)
{
_invoices = invoices;
_email = email;
}
public async Task ExecuteAsync(SendInvoiceInput input, CancellationToken ct)
{
var pdf = await _invoices.GenerateAsync(input.OrderId, ct);
await _email.SendAsync(input.CustomerEmail, pdf, ct);
}
}
2 — Register
// From appsettings.json (recommended)
builder.Services.AddNexJob(builder.Configuration)
.AddNexJobJobs(typeof(Program).Assembly);
// Or fluent
builder.Services.AddNexJob(opt =>
{
opt.UsePostgres(connectionString);
opt.Workers = 10;
});
3 — Schedule
// Fire and forget
await scheduler.EnqueueAsync<SendInvoiceJob, SendInvoiceInput>(new(orderId, email));
// Delayed
await scheduler.ScheduleAsync<SendInvoiceJob, SendInvoiceInput>(
new(orderId, email), delay: TimeSpan.FromMinutes(5));
// Recurring (cron)
await scheduler.RecurringAsync<MonthlyReportJob, MonthlyReportInput>(
id: "monthly-report",
input: new(DateTime.UtcNow.Month),
cron: "0 9 1 * *");
// Continuation — runs only after parent succeeds
var jobId = await scheduler.EnqueueAsync<ProcessPaymentJob, PaymentInput>(paymentInput);
await scheduler.ContinueWithAsync<SendReceiptJob, ReceiptInput>(jobId, receiptInput);
// With idempotency key — safe to call multiple times
await scheduler.EnqueueAsync<SendInvoiceJob, SendInvoiceInput>(
new(orderId, email),
idempotencyKey: $"invoice-{orderId}");
// With tags — searchable metadata
await scheduler.EnqueueAsync<SendInvoiceJob, SendInvoiceInput>(
new(orderId, email),
tags: ["tenant:acme", $"invoice:{invoiceId}"]);
4 — Dashboard
Web API / ASP.NET Core:
app.UseNexJobDashboard("/dashboard");
Worker Service / Console App:
services.AddNexJobStandaloneDashboard(configuration);
// Dashboard available at http://localhost:5005/dashboard
Open /dashboard to see every queue, every job state, every retry — live.
Configuration via appsettings.json
Every NexJob setting can live in appsettings.json. No code changes to tune behavior across environments.
{
"NexJob": {
"Workers": 10,
"DefaultQueue": "default",
"MaxAttempts": 5,
"ShutdownTimeoutSeconds": 30,
"PollingInterval": "00:00:05",
"HeartbeatInterval": "00:00:30",
"HeartbeatTimeout": "00:05:00",
"Queues": [
{ "Name": "critical", "Workers": 3 },
{ "Name": "default", "Workers": 5 },
{
"Name": "reports",
"Workers": 2,
"ExecutionWindow": {
"StartTime": "22:00",
"EndTime": "06:00",
"TimeZone": "America/Sao_Paulo"
}
},
{ "Name": "low", "Workers": 1 }
],
"Dashboard": {
"Path": "/dashboard",
"Title": "MyApp Jobs",
"RequireAuth": false,
"PollIntervalSeconds": 3
}
}
}
Use different files per environment — no code changes needed:
// appsettings.Development.json
{ "NexJob": { "Workers": 2, "PollingInterval": "00:00:01" } }
Execution windows
Restrict queues to specific time windows — without touching a single cron expression.
{
"NexJob": {
"Queues": [
{
"Name": "reports",
"ExecutionWindow": {
"StartTime": "22:00",
"EndTime": "06:00",
"TimeZone": "America/Sao_Paulo"
}
}
]
}
}
The reports queue only processes jobs between 10 PM and 6 AM São Paulo time.
Windows that cross midnight work naturally: 22:00 → 06:00 runs after 10 PM or before 6 AM.
Live configuration via dashboard
Change NexJob behavior at runtime — no restarts, no redeployments.
Open /jobs/settings in the dashboard to:
- Adjust workers — drag a slider, applied instantly across all instances
- Pause a queue — toggle any queue on/off without touching code
- Pause all recurring jobs — one click to freeze all cron-based jobs
- Change polling interval — tune live
- View effective config — see the merged result of appsettings + runtime overrides
- Reset to appsettings — clear all runtime overrides in one click
Priority queues
Jobs with Critical priority jump the queue. No workarounds, no separate deployments.
await scheduler.EnqueueAsync<AlertJob, AlertInput>(
input, priority: JobPriority.Critical); // Critical → High → Normal → Low
Resource throttling
Don't overwhelm external APIs. Declare a limit once, NexJob enforces it across all workers.
[Throttle(resource: "stripe", maxConcurrent: 3)]
public class ChargeCardJob : IJob<ChargeInput> { ... }
[Throttle(resource: "sendgrid", maxConcurrent: 5)]
public class BulkEmailJob : IJob<EmailInput> { ... }
All jobs sharing the same resource name share the same concurrency slot — globally, across all server instances.
Per-job retry configuration
Override the global retry policy per job type.
// 5 retries, doubling delay from 30s up to 1h
[Retry(5, InitialDelay = "00:00:30", Multiplier = 2.0, MaxDelay = "01:00:00")]
public class PaymentJob : IJob<PaymentInput> { ... }
// Dead-letter immediately on first failure — no retries
[Retry(0)]
public class WebhookJob : IJob<WebhookInput> { ... }
Global default (when no [Retry] is applied):
| Attempt | Delay |
|---|---|
| 1 | ~16 seconds |
| 2 | ~1 minute |
| 3 | ~5 minutes |
| 4 | ~17 minutes |
| 5 | ~42 minutes |
Job context
Inject IJobContext via DI to access runtime information inside any job — no changes to the IJob<TInput> interface required.
public class ImportCsvJob : IJob<ImportCsvInput>
{
private readonly IJobContext _ctx;
private readonly ILogger<ImportCsvJob> _logger;
public ImportCsvJob(IJobContext ctx, ILogger<ImportCsvJob> logger)
{
_ctx = ctx;
_logger = logger;
}
public async Task ExecuteAsync(ImportCsvInput input, CancellationToken ct)
{
_logger.LogInformation(
"Job {JobId} — attempt {Attempt}/{Max} — queue {Queue}",
_ctx.JobId, _ctx.Attempt, _ctx.MaxAttempts, _ctx.Queue);
await _ctx.ReportProgressAsync(0, "Starting import...", ct);
var rows = await LoadRowsAsync(input.FilePath, ct);
await foreach (var row in rows.WithProgress(_ctx, ct))
{
await ProcessRowAsync(row, ct);
}
await _ctx.ReportProgressAsync(100, "Done.", ct);
}
}
IJobContext exposes:
JobId— the current job's identifierAttempt— current attempt number (1-based)MaxAttempts— total attempts allowedQueue— queue the job was fetched fromRecurringJobId— set when the job was fired by a recurring definitionTags— tags attached at enqueue timeReportProgressAsync(int percent, string? message, CancellationToken)— updates the dashboard live
Job progress tracking
WithProgress automatically reports progress as you iterate — no manual calls needed.
// Works with IAsyncEnumerable
await foreach (var record in dbReader.ReadAllAsync(ct).WithProgress(_ctx, ct))
{
await ImportAsync(record, ct);
}
// Works with IEnumerable
foreach (var item in items.WithProgress(_ctx))
{
await ProcessAsync(item, ct);
}
The dashboard shows a live progress bar for any job that reports progress, updated in real time via SSE — no polling, no page refresh.
Job tags
Tag jobs at enqueue time to add searchable metadata:
await scheduler.EnqueueAsync<SendInvoiceJob, SendInvoiceInput>(
input,
tags: ["tenant:acme", $"order:{orderId}", "region:us-east"]);
Filter by tag in the dashboard or query programmatically:
var jobs = await scheduler.GetJobsByTagAsync("tenant:acme");
Schema migrations
NexJob automatically migrates its storage schema on startup. No manual SQL scripts, no deployment steps. Each migration runs in a transaction protected by a distributed advisory lock — safe when multiple instances start simultaneously.
// Nothing to call — migrations run automatically when your app starts
builder.Services.AddNexJob(builder.Configuration);
Graceful shutdown
When your host receives SIGTERM (Kubernetes rolling deployments, scale-down), NexJob waits for active jobs to complete before stopping.
{
"NexJob": {
"ShutdownTimeoutSeconds": 30
}
}
Jobs still running after the timeout are requeued automatically by the orphan watcher.
Observability
NexJob emits OpenTelemetry spans and metrics for every job — enqueue, execute, retry, fail. Zero extra configuration if you already have OTEL wired up.
builder.Services.AddOpenTelemetry()
.WithTracing(t => t.AddSource("NexJob"))
.WithMetrics(m => m.AddMeter("NexJob"));
Every execution span carries:
nexjob.job_id = "3f2a8c1d-..."
nexjob.job_type = "SendInvoiceJob"
nexjob.queue = "default"
nexjob.attempt = 1
nexjob.status = "succeeded"
nexjob.duration_ms = 142
Metrics: nexjob.jobs.enqueued, nexjob.jobs.succeeded, nexjob.jobs.failed, nexjob.job.duration.
Payload versioning
Your job inputs will change. NexJob handles it gracefully — no data loss, no broken jobs stuck in the queue.
[SchemaVersion(2)]
public class SendInvoiceJob : IJob<SendInvoiceInputV2> { ... }
// Migration — discovered and applied automatically before execution
public class SendInvoiceMigration : IJobMigration<SendInvoiceInputV1, SendInvoiceInputV2>
{
public SendInvoiceInputV2 Migrate(SendInvoiceInputV1 old)
=> new(old.OrderId, old.Email, Language: "en-US");
}
Register the migration at startup:
builder.Services.AddJobMigration<SendInvoiceInputV1, SendInvoiceInputV2, SendInvoiceMigration>();
Health checks
builder.Services.AddHealthChecks()
.AddNexJob() // Healthy / Degraded / Unhealthy
.AddNexJob(failureThreshold: 100); // Degraded if > 100 dead-letter jobs
Testing
The in-memory provider requires zero setup.
services.AddNexJob(opt => opt.UseInMemory());
Storage providers
All open-source. No license walls. Ever.
| Package | Storage | Notes |
|---|---|---|
NexJob |
In-memory | Dev and testing only |
NexJob.Postgres |
PostgreSQL 14+ | SELECT FOR UPDATE SKIP LOCKED + auto-migrations |
NexJob.SqlServer |
SQL Server 2019+ | UPDLOCK READPAST + auto-migrations |
NexJob.Redis |
Redis 7+ | Lua scripts for atomicity |
NexJob.MongoDB |
MongoDB 6+ | Atomic findAndModify |
NexJob.Oracle |
Oracle 19c+ | SKIP LOCKED |
Bring your own? Implement IStorageProvider — one interface, full XML docs on every method.
Configuration reference
builder.Services.AddNexJob(builder.Configuration, opt =>
{
// Storage — pick one
opt.UsePostgres(connectionString);
opt.UseSqlServer(connectionString);
opt.UseRedis(connectionString);
opt.UseMongoDB(connectionString, databaseName: "nexjob");
opt.UseOracle(connectionString);
opt.UseInMemory(); // dev/tests
// Workers & queues (override appsettings)
opt.Workers = 10;
opt.Queues = ["critical", "default", "low"];
opt.DefaultQueue = "default";
// Timing
opt.PollingInterval = TimeSpan.FromSeconds(5);
opt.HeartbeatInterval = TimeSpan.FromSeconds(30);
opt.HeartbeatTimeout = TimeSpan.FromMinutes(5);
opt.ShutdownTimeout = TimeSpan.FromSeconds(30);
// Retries
opt.MaxAttempts = 5;
});
Roadmap
v0.1.0-alpha ◆ Core · all storage providers · dashboard · 55 tests
v0.2.0 ◆ Schema migrations · graceful shutdown · [Retry] · distributed lock · 130 tests
v0.3.0 ◆ IJobContext · progress tracking · job tags
v1.0.0 ○ Stable API · production-ready
Contributing
NexJob is built in the open. Issues, ideas, and PRs are welcome.
git clone git@github.com:oluciano/NexJob.git
cd NexJob
dotnet restore
dotnet test
Read CONTRIBUTING.md before opening a PR.
License
MIT © 2025 Luciano Azevedo
<div align="center"> <br/>
Built with obsession over developer experience.
If Hangfire is the past, NexJob is what comes next.
<br/> </div>
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Cronos (>= 0.8.4)
- Microsoft.Extensions.Configuration.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Configuration.Binder (>= 8.0.2)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 8.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.1)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.3)
- System.Diagnostics.DiagnosticSource (>= 8.0.1)
NuGet packages (6)
Showing the top 5 NuGet packages that depend on NexJob:
| Package | Downloads |
|---|---|
|
NexJob.Dashboard
Blazor SSR dashboard middleware for NexJob — real-time monitoring of background jobs. |
|
|
NexJob.Postgres
PostgreSQL storage provider for NexJob — the open-source background job scheduler for .NET 8+. |
|
|
NexJob.MongoDB
MongoDB storage provider for NexJob — the open-source background job scheduler for .NET 8+. |
|
|
NexJob.SqlServer
SQL Server storage provider for NexJob. |
|
|
NexJob.Redis
Redis storage provider for NexJob. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 83 | 4/8/2026 |
| 0.8.0 | 79 | 4/8/2026 |
| 0.7.0 | 123 | 4/7/2026 |
| 0.6.0 | 162 | 4/3/2026 |
| 0.5.2 | 168 | 4/1/2026 |
| 0.5.1 | 158 | 4/1/2026 |
| 0.5.0 | 168 | 3/31/2026 |
| 0.4.0 | 163 | 3/31/2026 |
| 0.3.2 | 206 | 3/28/2026 |
| 0.3.1 | 219 | 3/28/2026 |
| 0.3.0 | 181 | 3/27/2026 |
| 0.2.0 | 178 | 3/27/2026 |
| 0.1.1 | 131 | 3/27/2026 |
| 0.1.0 | 122 | 3/26/2026 |
| 0.1.0-alpha | 123 | 3/26/2026 |