Migrating from Hangfire
If you are running Hangfire today, you already understand background job processing in .NET. Zeridion Flare uses the same mental model — define a job, enqueue it, let a worker execute it — but replaces the self-hosted infrastructure with a managed service. Most projects can complete the migration in under an hour: swap the NuGet package, convert job classes to IJob<T>, and delete your Redis or SQL Server storage.
Why migrate
Hangfire is battle-tested, but operating it means owning the entire stack. Zeridion Flare removes that burden:
- No storage to maintain — no SQL Server tables, no Redis instance, no connection string rotation. Job storage is a managed PostgreSQL cluster you never touch.
- No dashboard deployment — Hangfire Dashboard requires its own middleware, auth configuration, and reverse-proxy rules. Zeridion Flare's dashboard is hosted at
zeridion.comand available the moment you create an API key. - Automatic retry with backoff and jitter — exponential backoff (
15s × 2^attempt + jitter) is the default. No[AutomaticRetry]tuning required. - Native job continuations — parent-child chaining via
ContinueWithAsyncwith automatic child cancellation when a parent dead-letters. - Progress reporting (beta) — call
ctx.ReportProgress(0.0–1.0)from inside any job. In the current SDK (0.1.0-beta.1),ReportProgressis a no-op placeholder; live dashboard rendering will be wired in a future release. - Managed observability — OpenTelemetry tracing with
TenantIdon every span, plus a metrics API for custom dashboards. - Zero servers to patch — your app talks to
api.zeridion.comover HTTPS. Scaling, failover, and upgrades are handled for you.
Concept mapping
Use this table as a quick reference while converting your codebase. The left column shows the Hangfire API; the right column shows the Zeridion Flare equivalent.
| Hangfire | Zeridion Flare |
|---|---|
BackgroundJob.Enqueue(() => ...) | await jobs.EnqueueAsync<TJob>(payload) |
BackgroundJob.Enqueue<T>(x => x.Method(args)) | await jobs.EnqueueAsync<TJob>(payload) |
BackgroundJob.Schedule(() => ..., delay) | await jobs.ScheduleAsync<TJob>(payload, delay) |
BackgroundJob.Schedule(() => ..., DateTimeOffset) | await jobs.ScheduleAsync<TJob>(payload, runAt) |
BackgroundJob.ContinueJobWith(parentId, ...) | await jobs.ContinueWithAsync<TJob>(parentId, payload) |
RecurringJob.AddOrUpdate("id", () => ..., cron) | [JobConfig(CronSchedule = "...")] on an IRecurringJob class |
RecurringJob.RemoveIfExists("id") | DELETE /v1/recurring/{id} via API |
BackgroundJob.Delete(jobId) | await jobs.CancelAsync(jobId) |
BackgroundJob.Requeue(jobId) | await jobs.RetryAsync(jobId) |
IJobCancellationToken | JobContext.CancellationToken |
[AutomaticRetry(Attempts = N)] | [JobConfig(MaxAttempts = N)] or JobOptions.MaxAttempts |
[Queue("name")] | [JobConfig(Queue = "name")] or JobOptions.Queue |
JobStorage (SQL Server / Redis) | Managed PostgreSQL (zero config) |
Hangfire.Dashboard middleware | Managed dashboard at zeridion.com |
GlobalConfiguration.Configuration | AddZeridionFlare(options => { ... }) |
app.UseHangfireServer() | app.UseZeridionFlare() (extends IHost) |
app.UseHangfireDashboard() | No equivalent needed — dashboard is hosted |
PerformContext | JobContext |
JobFilterAttribute | No direct equivalent (server-side middleware) |
Step-by-step migration
Step 1: Swap NuGet packages
Remove all Hangfire packages and add the Zeridion Flare SDK:
dotnet remove package Hangfire.Core
dotnet remove package Hangfire.AspNetCore
dotnet remove package Hangfire.SqlServer # or Hangfire.Redis.StackExchange
dotnet add package Zeridion.Flare --prerelease
Step 2: Replace startup configuration
Before (Hangfire):
builder.Services.AddHangfire(config => config
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseSqlServerStorage(connectionString));
builder.Services.AddHangfireServer();
var app = builder.Build();
app.UseHangfireDashboard();
After (Zeridion Flare):
builder.Services.AddZeridionFlare(options =>
{
options.ApiKey = builder.Configuration["Zeridion:ApiKey"]!;
});
var app = builder.Build();
app.UseZeridionFlare();
No connection string, no storage configuration, no dashboard middleware. AddZeridionFlare registers the HTTP client, job type registry, worker service, and all DI bindings in a single call.
Step 3: Convert job classes
Hangfire jobs are typically methods on a service class, activated via expression trees. Zeridion Flare jobs are dedicated classes that implement IJob<T> with a strongly typed payload.
Before (Hangfire):
public class EmailService
{
[AutomaticRetry(Attempts = 5)]
[Queue("email")]
public async Task SendWelcomeEmail(string userId, string email)
{
// send email logic
}
}
After (Zeridion Flare):
public sealed record WelcomeEmailPayload
{
public required string UserId { get; init; }
public required string Email { get; init; }
}
[JobConfig(Queue = "email", MaxAttempts = 5)]
public sealed class SendWelcomeEmail(
IEmailService emailService,
ILogger<SendWelcomeEmail> logger) : IJob<WelcomeEmailPayload>
{
public async Task ExecuteAsync(WelcomeEmailPayload payload, JobContext ctx)
{
await emailService.SendAsync(payload.Email, "Welcome!", ctx.CancellationToken);
}
}
- Typed payload — a
recordreplaces loose method parameters. The payload is serialized to JSON and stored with the job. - Constructor injection — dependencies are injected via the primary constructor. No service locator or
Activatepattern. - CancellationToken — provided through
JobContext.CancellationToken, notIJobCancellationToken. - Single attribute —
[JobConfig]replaces both[AutomaticRetry]and[Queue].
Step 4: Replace enqueue calls
Hangfire uses the static BackgroundJob class with expression trees. Zeridion Flare uses the IJobClient interface injected through DI.
Before (Hangfire):
BackgroundJob.Enqueue<EmailService>(x => x.SendWelcomeEmail(userId, email));
BackgroundJob.Schedule<EmailService>(
x => x.SendWelcomeEmail(userId, email),
TimeSpan.FromHours(1));
BackgroundJob.ContinueJobWith<NotifyService>(
parentId,
x => x.NotifyAdmin(userId));
After (Zeridion Flare):
var jobId = await jobs.EnqueueAsync<SendWelcomeEmail>(
new WelcomeEmailPayload { UserId = userId, Email = email });
await jobs.ScheduleAsync<SendWelcomeEmail>(
new WelcomeEmailPayload { UserId = userId, Email = email },
TimeSpan.FromHours(1));
await jobs.ContinueWithAsync<NotifyAdmin>(
jobId,
new NotifyPayload { UserId = userId });
All IJobClient methods are async and return the server-assigned job ID. Inject IJobClient through your constructor — the static BackgroundJob class has no equivalent.
Step 5: Convert recurring jobs
Hangfire recurring jobs are registered imperatively at startup. Zeridion Flare recurring jobs are classes that implement IRecurringJob with a [JobConfig(CronSchedule)] attribute — the SDK discovers and registers them automatically when the worker starts.
Before (Hangfire):
RecurringJob.AddOrUpdate<CleanupService>(
"expired-sessions",
x => x.CleanupExpiredSessions(),
"0 3 * * *",
new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
After (Zeridion Flare):
[JobConfig(CronSchedule = "0 3 * * *", Queue = "maintenance")]
public sealed class CleanupExpiredSessions(AppDbContext db) : IRecurringJob
{
public async Task ExecuteAsync(JobContext ctx)
{
await db.Sessions
.Where(s => s.ExpiresAt < DateTime.UtcNow)
.ExecuteDeleteAsync(ctx.CancellationToken);
}
}
No explicit RecurringJob.AddOrUpdate call is needed. The SDK scans your assemblies for IRecurringJob implementations that have a CronSchedule set in [JobConfig], then registers them with the API during worker startup. Cron expressions use the standard 5-field format (parsed by the Cronos library).
If you manage recurring jobs dynamically (adding/removing at runtime), use the REST API directly: PUT /v1/recurring/{id} to upsert and DELETE /v1/recurring/{id} to remove. See the Recurring Jobs API reference.
Configuration mapping
Map your Hangfire server options to the equivalent ZeridionFlareOptions properties:
| Hangfire setting | Flare equivalent | Default |
|---|---|---|
WorkerCount | ZeridionFlareOptions.ConcurrencyLimit | 10 |
Queues = new[] { "critical", "default" } | Per-job [JobConfig(Queue)] or JobOptions.Queue | "default" |
ServerTimeout | Per-job [JobConfig(TimeoutSeconds)] or JobOptions.Timeout | 30 minutes |
SchedulePollingInterval | ZeridionFlareOptions.PollInterval | 2 seconds |
| Connection string (SQL / Redis) | ZeridionFlareOptions.ApiKey | (required) |
| N/A | ZeridionFlareOptions.ApiBaseUrl | https://api.zeridion.com |
| N/A | ZeridionFlareOptions.JobAssemblies | Entry assembly |
ZeridionFlareOptions also has DefaultQueue, DefaultMaxAttempts, and DefaultTimeout properties, but they are not currently consumed by the enqueue path. Use [JobConfig] for per-class defaults or JobOptions for per-call overrides. See Option Precedence.
Options can also be bound from appsettings.json under the Zeridion section. See Configuration for the full property reference.
What changes in your architecture
With Hangfire, you own the entire pipeline — storage, server process, and dashboard. You provision the database, manage connection strings, configure retry policies, and deploy the dashboard behind authentication.
With Zeridion Flare, your application talks to the Zeridion API over HTTPS. Job storage, execution scheduling, retry logic, and the dashboard are all managed. Your app only needs a NuGet package and an API key.
Features you gain
Capabilities that Hangfire does not provide out of the box, or that require significant custom configuration:
- Exponential backoff with jitter — automatic on every retry, no attribute tuning needed
- Job continuations —
ContinueWithAsyncchains parent-child jobs with automatic child cancellation on parent failure - Progress reporting (beta) —
ctx.ReportProgress(0.0–1.0)API is available; live dashboard rendering is planned for a future release - Idempotency keys — set
JobOptions.IdempotencyKeyto prevent duplicate enqueues (API returns409 Conflict) - Per-job tags — attach key-value tags via
JobOptions.Tagsfor filtering and grouping in the dashboard - Managed monitoring — OpenTelemetry tracing with
TenantIdon every span, plus metrics endpoints for summary, throughput, and queue depth - Rate limiting —
X-RateLimit-Limit,X-RateLimit-Remaining, andX-RateLimit-Resetheaders on every API response
Migration checklist
Copy this checklist into your issue tracker and work through it sequentially:
- Remove all Hangfire NuGet packages (
Hangfire.Core,Hangfire.AspNetCore,Hangfire.SqlServer, etc.) - Remove
GlobalConfiguration,AddHangfire,AddHangfireServer, andUseHangfireDashboardfrom startup - Add the
Zeridion.FlareNuGet package - Add
AddZeridionFlareandUseZeridionFlaretoProgram.cs - Convert each Hangfire job method to an
IJob<T>class with a typed payload record - Convert recurring job registrations to
IRecurringJobclasses with[JobConfig(CronSchedule)] - Replace all
BackgroundJob.Enqueue/Schedule/ContinueJobWithcalls withIJobClientmethods - Replace
IJobCancellationTokenusage withJobContext.CancellationToken - Replace
PerformContextusage withJobContext(properties:JobId,AttemptNumber,MaxAttempts,Logger,ReportProgress) - Remove Hangfire storage infrastructure (SQL Server tables or Redis instance)
- Run the application and verify jobs execute via the managed dashboard at
zeridion.com
See also
- Quick Start — first job in 5 minutes
- IJob Interface —
ExecuteAsynccontract and payload constraints - IRecurringJob Interface — cron schedule and no-payload design
- IJobClient — all 7 methods: enqueue, schedule, continue, cancel, retry, status
- JobContext — properties available inside job execution
- Configuration — full
ZeridionFlareOptionsreference - Retry Strategies — backoff formula, dead letter behavior, manual retry
- Error Handling — SDK exception hierarchy and catch patterns