Skip to main content

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.com and 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 ContinueWithAsync with 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), ReportProgress is a no-op placeholder; live dashboard rendering will be wired in a future release.
  • Managed observability — OpenTelemetry tracing with TenantId on every span, plus a metrics API for custom dashboards.
  • Zero servers to patch — your app talks to api.zeridion.com over 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.

HangfireZeridion 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)
IJobCancellationTokenJobContext.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 middlewareManaged dashboard at zeridion.com
GlobalConfiguration.ConfigurationAddZeridionFlare(options => { ... })
app.UseHangfireServer()app.UseZeridionFlare() (extends IHost)
app.UseHangfireDashboard()No equivalent needed — dashboard is hosted
PerformContextJobContext
JobFilterAttributeNo 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);
}
}
Key differences
  • Typed payload — a record replaces 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 Activate pattern.
  • CancellationToken — provided through JobContext.CancellationToken, not IJobCancellationToken.
  • 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 });
note

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).

warning

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 settingFlare equivalentDefault
WorkerCountZeridionFlareOptions.ConcurrencyLimit10
Queues = new[] { "critical", "default" }Per-job [JobConfig(Queue)] or JobOptions.Queue"default"
ServerTimeoutPer-job [JobConfig(TimeoutSeconds)] or JobOptions.Timeout30 minutes
SchedulePollingIntervalZeridionFlareOptions.PollInterval2 seconds
Connection string (SQL / Redis)ZeridionFlareOptions.ApiKey(required)
N/AZeridionFlareOptions.ApiBaseUrlhttps://api.zeridion.com
N/AZeridionFlareOptions.JobAssembliesEntry assembly
note

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 continuationsContinueWithAsync chains 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.IdempotencyKey to prevent duplicate enqueues (API returns 409 Conflict)
  • Per-job tags — attach key-value tags via JobOptions.Tags for filtering and grouping in the dashboard
  • Managed monitoring — OpenTelemetry tracing with TenantId on every span, plus metrics endpoints for summary, throughput, and queue depth
  • Rate limitingX-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers 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, and UseHangfireDashboard from startup
  • Add the Zeridion.Flare NuGet package
  • Add AddZeridionFlare and UseZeridionFlare to Program.cs
  • Convert each Hangfire job method to an IJob<T> class with a typed payload record
  • Convert recurring job registrations to IRecurringJob classes with [JobConfig(CronSchedule)]
  • Replace all BackgroundJob.Enqueue / Schedule / ContinueJobWith calls with IJobClient methods
  • Replace IJobCancellationToken usage with JobContext.CancellationToken
  • Replace PerformContext usage with JobContext (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