Coravel vs Hangfire: when the simpler one is the right answer
Coravel vs Hangfire for .NET scheduling — zero-infrastructure fluent scheduling against a persistent, dashboarded job system, and how to tell which you need.
This comparison usually gets framed as “lightweight vs full-featured”, which makes it sound like Coravel is the compromise choice. It isn’t. It’s a different bet about where job state should live — and for a lot of applications it’s the correct bet.
The one difference everything follows from
Coravel keeps schedule state in memory. Hangfire keeps it in a database.
Every other difference is downstream of that:
| Coravel | Hangfire | |
|---|---|---|
| Infrastructure required | None | SQL Server, PostgreSQL, Redis, … |
| Setup | AddScheduler(), one lambda | Storage, migrations, dashboard auth |
| Survives a restart | No | Yes |
| Two instances | Both fire every job | Coordinated via storage |
| Job history | None | Full, in the dashboard |
| Retries | You write them | Automatic, configurable |
| Fire-and-forget from a request | Yes (in-memory queue) | Yes (persistent) |
| Also does | Caching, mailing, event broadcasting | Batches, continuations (Pro) |
| Licence | MIT | LGPL v3 core, paid Pro |
Coravel’s setup is genuinely this small:
services.AddScheduler();
app.Services.UseScheduler(scheduler =>
{
scheduler.Schedule<NightlyCleanup>().DailyAt(2, 0);
scheduler.Schedule<SyncInventory>().EveryFifteenMinutes().PreventOverlapping("sync");
});
No tables, no migrations, no dashboard route to secure. For a small app that is not a lesser version of Hangfire — it’s the absence of a problem.
What in-memory actually costs
Be concrete about it, because the trade only works if you’re honest:
- A restart loses the schedule’s state. Deploy at 01:59 and the 02:00 job doesn’t run late — it doesn’t run. There’s no store to notice it was missed.
- Horizontal scaling breaks the semantics. Three instances means three
copies of every scheduled job firing simultaneously.
PreventOverlappingis per-process; it does not coordinate across instances. Your job handlers have to be idempotent, or you stay at one instance. - There’s no history. When someone asks whether last Tuesday’s job ran, the answer is whatever your logs happened to capture.
- Retries are yours to write. Coravel will not retry a failed job for you.
If none of those four sentences describe a problem you have, Coravel is the better tool, and installing Hangfire instead means running a database dependency to solve problems you don’t have.
When to pick Hangfire
- You run more than one instance of the app. This is usually the whole decision — it’s the one Coravel limitation you cannot engineer around from the outside.
- Jobs are financially or legally consequential and “it silently didn’t run” is unacceptable.
- You need job history for debugging or for an auditor.
- You enqueue substantial fire-and-forget work from request handlers and need it to survive a crash. Coravel has an in-memory queue; a crash empties it.
- You want retries with backoff without writing them.
When to pick Coravel
- Single instance. A monolith on one VM, a container that scales vertically, an internal tool.
- Jobs are idempotent and recoverable — a sync that catches up next run, a cache warm, a digest email that can be a few minutes late.
- You’d otherwise be adding a database purely for the scheduler.
- You want its other pieces too. Coravel’s caching, mailing, and event broadcasting often replace three more dependencies, which is a real argument that has nothing to do with scheduling.
One thing to check before you commit: Coravel’s release cadence has slowed considerably — the latest NuGet package is 6.0.2 from January 2025. For a small, stable library that does one thing, “finished” is a legitimate state and not the same as abandoned. But if you need a fix landed on someone else’s timetable, weigh that. Hangfire and Quartz.NET both ship far more frequently.
The middle path most people miss
The framing above assumes the choice is which library runs inside my app. For recurring jobs specifically, there’s a third option: don’t keep the schedule in the process at all.
If the job is “call this endpoint every night at 02:00”, the schedule can live outside, and your handler stays exactly where it is:
jobs:
- id: nightly-cleanup
name: Nightly cleanup
kind: http
method: POST
url: https://app.example.com/internal/jobs/cleanup
schedule: "0 2 * * *"
timezone: Europe/Berlin
retries: 3
headers:
Authorization: Bearer ${CRON_SECRET}
That gets you Coravel’s zero-infrastructure story and the durability Hangfire uses a database for: the schedule survives restarts because it was never in the process, multiple instances don’t multiply the job because only one HTTP call is made, and you get retries and an execution log without a job table. Full trade-off: Hangfire alternative.
It genuinely doesn’t help with fire-and-forget work enqueued from a request handler. Keep Coravel’s queue or Hangfire’s for that.
Either way: notice when it stops
Coravel’s in-memory model makes silent failure more likely — there’s no store holding evidence that a job was due — but Hangfire fails quietly too when the process is down. Neither library can alert you about its own death.
Whichever you pick, put one outside witness on the jobs that matter: the job pings when it finishes, and a missing ping becomes the alert. The .NET SDK is one attribute, and heartbeat monitoring is free down to once a minute. For a single-instance Coravel app it’s the cheapest possible insurance against the failure mode you actually signed up for.
Still choosing? The four-way comparison: Hangfire vs Quartz.NET vs TickerQ vs Coravel.