Skip to content

Repository files navigation

Immediate.Jobs

NuGetGitHub releaseGitHub licenseGitHub issuesGitHub issues closedGitHub ActionsCoverage StatusDocs

Immediate.Jobs is a reflection-free background job scheduler for .NET 8+ built on Immediate.Handlers. A job is a [Handler] whose request can also be durably enqueued; a Roslyn source generator emits its typed scheduler, payload metadata, and dependency-injection registrations at compile time.

Important

Immediate.Jobs provides at-least-once delivery. Every handler that performs externally visible work must be idempotent. The in-memory provider is single-node, non-durable, and intended only for development, tests, and non-critical work.

Quick start

Install the core package:

dotnet add package Immediate.Jobs --prerelease

Define a job using the same handler model as Immediate.Handlers, then inject its generated scheduler:

usingImmediate.Handlers.Shared;usingImmediate.Jobs.Shared;[Handler,Job(Name="send-welcome-email",MaxAttempts=5)]publicsealedpartialclassSendWelcomeEmail(IEmailSendersender){publicsealedrecordPayload(GuidUserId,stringTemplate);privateValueTaskHandleAsync(Payloadpayload,CancellationTokencancellationToken)=>new(sender.SendAsync(payload.UserId,payload.Template,cancellationToken));}publicsealedclassSignupService(SendWelcomeEmail.SchedulerwelcomeEmail){publicValueTask<JobHandle>EnqueueAsync(GuiduserId,CancellationTokencancellationToken)=>welcomeEmail.EnqueueAsync(new(userId,"v2"),cancellationToken);}

Register the generated handlers and jobs methods in Program.cs, in that order. For an assembly named MyApp, these are AddMyAppHandlers() and AddMyAppJobs().

Scheduling and handles

Generated schedulers use one method name for relative and absolute scheduling. Pass a TimeSpan for a delay or a DateTimeOffset for a due time:

JobHandleimmediate=awaitwelcomeEmail.EnqueueAsync(new(userId,"v2"),cancellationToken);JobHandledelayed=awaitwelcomeEmail.ScheduleAsync(new(userId,"reminder"),TimeSpan.FromHours(1),cancellationToken);JobHandlescheduled=awaitwelcomeEmail.ScheduleAsync(new(userId,"tomorrow"),DateTimeOffset.UtcNow.AddDays(1),cancellationToken);

JobHandle and BatchHandle keep job and batch identifiers separate in application code. Read their string values from JobHandle.JobHandle and BatchHandle.BatchHandle. Use JobHandle.FromString(...) or BatchHandle.FromString(...) when an identifier enters through a route, message, or another string-based boundary. Both handle types serialize to their string value with System.Text.Json.

Batches and continuations

Build an atomic workflow with BatchScheduler and the generated scheduler methods. Enqueue creates a root job. ScheduleAfter accepts one or several earlier BatchJobHandle values, which supports fan-out and fan-in without persisting a partial graph:

awaitusingvarbatch=batches.Begin();varreceived=receiveOrder.Enqueue(new(orderId),batch);varinventory=reserveInventory.ScheduleAfter(new(orderId),received);varpayment=capturePayment.ScheduleAfter(new(orderId),received);vardispatch=dispatchOrder.ScheduleAfter(new(orderId),[inventory,payment]);BatchHandlebatchHandle=awaitbatch.CommitAsync(cancellationToken);JobHandledispatchHandle=dispatch.JobHandle;

The batch builder is short-lived and is not thread-safe. Nothing reaches storage until CommitAsync succeeds. A BatchJobHandle exposes its JobHandle only after that commit. Disposing an uncommitted batch discards its buffered jobs.

Use ScheduleAfterAsync for a continuation created outside an open batch. Its parent may be a JobHandle or BatchHandle, and a list of handles creates a fan-in dependency. Delays start when the required parent outcome is reached:

JobHandlefollowUp=awaitsendReceipt.ScheduleAfterAsync(new(orderId),batchHandle,TimeSpan.FromMinutes(5),cancellationToken:cancellationToken);

Jobs can also extend their current workflow. Implement IJobRequest on the payload to receive JobDetails, then call ScheduleAfter from the handler. The runtime writes the buffered additions only when that attempt succeeds, so a retry does not leave duplicate branches:

publicsealedrecordPayload(GuidOrderId):IJobRequest{publicJobDetails?JobDetails{get;set;}}privateValueTaskHandleAsync(Payloadpayload,CancellationTokencancellationToken){varcurrentJob=payload.JobDetails??thrownewInvalidOperationException("Job details were not populated.");recordAssessment.ScheduleAfter(new(payload.OrderId),currentJob,ContinuationOptions.BeforeContinuations);returnValueTask.CompletedTask;}

BeforeContinuations makes existing waiters depend on the new job. BesideContinuations adds a parallel branch, and Detached schedules outside the current batch. The asynchronous EnqueueAsync and ScheduleAsync overloads that accept JobDetails persist a new member in the current batch immediately when the work must not wait for the running attempt to finish.

Packages

Each package has focused installation and configuration guidance:

PackagePurpose
Immediate.JobsCore scheduler, source generator, execution engine, and in-memory provider
Immediate.Jobs.EntityFrameworkCoreDurable EF Core storage for PostgreSQL, SQLite, and SQL Server
Immediate.Jobs.LinqToDBDurable LinqToDB storage for PostgreSQL, SQLite, and SQL Server
Immediate.Jobs.RedisDistributed Redis queue and recurring storage
Immediate.Jobs.DashboardEmbedded monitoring dashboard and HTTP API
Immediate.Jobs.TestingDeterministic test harness, test doubles, assertions, and provider conformance tests
Immediate.Jobs.NodaTimeNodaTime scheduling overloads and job payload serialization

The SQL providers support batches, continuations, and fair scheduling between tenant groups. Redis does not support those features in the current release. See Queues and fairness and Batches and continuations for details.

Samples and documentation

The online documentation covers the complete API. The Aspire sample runs the EF Core provider against an Aspire-managed PostgreSQL container, exports logs, traces, metrics, and health status, and exposes the Immediate.Jobs dashboard.

Benchmarks

The repository includes BenchmarkDotNet comparisons with TickerQ, Hangfire MemoryStorage, and Quartz.NET. In addition to enqueue, direct dispatch, and startup, the suite covers concurrent throughput, cron expressions, delegate invocation, job creation, serialization, and startup registration. These are microbenchmarks of deliberately different framework APIs—not end-to-end durability or worker-latency measurements—so run them on the deployment target before drawing conclusions.

Results

The tables below are the historical ShortRun results from 21 July 2026: BenchmarkDotNet 0.15.8, .NET 8.0.22 Arm64 RyuJIT, Apple M3 Pro with 12 cores, macOS 26.5. Each result uses one launch, three warmup iterations, and three measurement iterations. Ratios use Immediate.Jobs as the baseline. The expanded TickerQ suite targets .NET 10 and does not yet have checked-in results.

EnqueueAsync

FrameworkMeanRatioAllocatedAllocation ratio
Immediate.Jobs3.796 μs1.005.07 KB1.00
Hangfire16.122 μs4.2514.49 KB2.86
Quartz.NET17.288 μs4.566.34 KB1.25

Direct dispatch

FrameworkMeanRatioAllocated
Immediate.Jobs0.9994 ns1.000 B
Hangfire28.0701 ns28.0932 B
Quartz.NET0.0521 ns0.050 B

The Immediate.Jobs and Quartz.NET dispatch operations are effectively below the benchmark's reliable measurement floor. Treat their sub-nanosecond values as "no measurable dispatch overhead" rather than literal timing precision.

Scheduler construction

FrameworkMeanRatioAllocatedAllocation ratio
Immediate.Jobs393.60 ns1.00649 B1.00
Hangfire7,765.75 ns19.773,104 B4.78
Quartz.NET11.67 ns0.03136 B0.21

The checked-in reports are available for enqueue, direct dispatch, and scheduler construction.

Run the complete suite with:

dotnet run --project benchmarks/Immediate.Jobs.Benchmarks -c Release -- --filter '*'

License

Immediate.Jobs is licensed under the MIT License.

About

Immediate.Jobs is a fast, reflection-free background task scheduler for .NET — built with source generators; supporting cron + time-based execution, and a real-time dashboard.

Resources

Stars

11 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages