Skip to main content

Jobs

Jobs implement IJob and have a single handler. They support scheduling, retries, continuations, batches, named queues, and mutex.

Define a job

public class GenerateReport : IJob
{
public int ReportId { get; set; }
}

public class GenerateReportHandler : IJobHandler<GenerateReport>
{
public async Task HandleAsync(GenerateReport message, CancellationToken ct)
{
// Generate the report
}
}

Enqueue

await publisher.Enqueue(new GenerateReport { ReportId = 1 });

Schedule

await publisher.Schedule(new GenerateReport { ReportId = 1 }, DateTime.UtcNow.AddHours(1));

Retries

Declare retry policy on the handler:

[Retry(3)]
public class GenerateReportHandler : IJobHandler<GenerateReport>
{
// ...
}

Or on the job class:

[Retry(3, Delays = [15, 60, 300])]
public class GenerateReport : IJob
{
public int ReportId { get; set; }
}

Override per-enqueue via metadata:

await publisher.Enqueue(new GenerateReport { ReportId = 1 },
new JobParameters().Configure<IRetryMetadata>(m => m.MaxRetries = 5));

Configure global defaults inside the AddWarpServer lambda:

builder.Services.AddWarpServer<AppDbContext>(opt =>
{
opt.AddRetry(o =>
{
o.MaxRetries = 3;
o.Delays = [15, 60, 300]; // seconds
o.JitterFactor = 0.2; // ±20% random jitter on each delay (default: 0, no jitter)
});
});

Priority: per-enqueue metadata > handler attribute > job attribute > global RetryOptions.

Failed jobs are retried automatically. Crash requeues (server died mid-execution) do not count against the retry limit.

JitterFactor is a multiplicative, global-only jitter applied to each computed delay: delay * (1 + JitterFactor * rand(-1, 1)). Clamped to [0, 1]. Useful when many jobs fail at the same time (e.g. downstream outage) to spread their retry attempts and avoid a thundering herd.

Named Queues

await publisher.Enqueue(new GenerateReport { ReportId = 1 }, queue: "critical");

Queues are processed in alphabetical order. A worker subscribes to specific queues:

options.Queues = new[] { "a-critical", "b-default", "c-low" };

Continuations

var parentId = await publisher.Enqueue(new ProcessPayment { OrderId = 1 });
await publisher.Enqueue(new SendReceipt { OrderId = 1 }, parentId); // Runs after parent completes

Batches

Batches use IBatchPublisher — a separate service from IPublisher. Inject it directly:

public class MyService(IBatchPublisher batchPublisher) { }
var jobs = orders.Select(o => new ProcessOrder { OrderId = o.Id }).ToList();
var batchId = await batchPublisher.StartNew(jobs);

// Continuation after batch completes
var followUps = new List<SendSummary> { new() };
await batchPublisher.ContinueBatchWith(followUps, batchId);

Batch continuation options control when continuations activate:

// Default: continuation only fires when ALL jobs succeed
await batchPublisher.StartNew(jobs, ContinuationOptions.OnlyOnSucceeded);

// Fire when all jobs finish, regardless of success/failure
await batchPublisher.StartNew(jobs, ContinuationOptions.OnAnyFinishedState);

With OnlyOnSucceeded (default): if any job in the batch fails, the batch itself transitions to Failed and continuations stay in Awaiting state indefinitely. You can requeue the failed jobs from the dashboard — if they succeed on retry and the batch completes, continuations will activate normally.

With OnAnyFinishedState: continuations fire as soon as all jobs reach a terminal state (Completed or Failed), regardless of outcome. Use this when the continuation needs to run even if some jobs failed (e.g., sending a summary report).

Recurring Jobs

Register a cron-based recurring job:

await recurringPublisher.AddOrUpdateRecurringJob(
new CleanupSessions(), name: "session-cleanup", cron: "0 * * * *");

This only registers the definition. The RecurringJobScheduler task creates jobs when the cron time arrives. See Recurring Jobs for full details.

Cancellation

Cancel a running job gracefully:

await jobCommandService.DeleteJob(jobId);

If the job is processing, this sets CancellationMode = Graceful instead of immediately changing state. The worker detects it and cancels the handler's CancellationToken. See Job Cancellation for the full flow.

Concurrency control (Mutex + Semaphore)

Cap the number of concurrent jobs per key. Requires opt.AddConcurrency() inside AddWarpServer. Use [Mutex] (WithMutex) for at-most-one, [Semaphore] (WithSemaphore) for at-most-N:

// At most one CallPaymentApi for this customer at a time
await publisher.Enqueue(new ProcessPayment { CustomerId = 123 },
new JobParameters().WithMutex("payment:123"));

// At most 5 concurrent calls to the payment API across all jobs
await publisher.Enqueue(new CallPaymentApi(),
new JobParameters().WithSemaphore("payment-api", limit: 5));

Or use attributes on the job class for static keys:

[Mutex("payment-processing")]
public class ProcessPayment : IJob { ... }

[Semaphore("payment-api", limit: 5)]
public class CallPaymentApi : IJob { ... }

See Concurrency control for details.