.NET and C# Cron Scheduling — A Complete Guide
The .NET scheduling landscape has three serious options: a built-in pattern (BackgroundService + Timer or NCrontab), the heavyweight Quartz.NET port of Java's Quartz, and Hangfire — a job queue with cron-style schedules and a built-in dashboard. This guide compares all three plus the lighter-weight NCrontab library.
Updated
The landscape
| Library | What it does | Best for |
|---|---|---|
| BackgroundService + Timer | DIY scheduling in ASP.NET Core | Simple intervals, single-process apps |
| NCrontab | Cron expression parsing + next-run | Adding cron syntax to a custom scheduler |
| Cronos | Modern alternative to NCrontab | Same use case; some prefer the API |
| Quartz.NET | Full-featured scheduler ported from Java Quartz | Complex schedules, persistence, clustering |
| Hangfire | Background job queue with cron scheduling + dashboard | Production apps wanting durability + observability |
| Coravel | Lightweight scheduler + queue for ASP.NET Core | Smaller apps that don’t need Hangfire’s full feature set |
For most ASP.NET Core apps, the progression is: start with BackgroundService → outgrow it for a single complex schedule → adopt Hangfire when you also need a job queue → adopt Quartz.NET only if you specifically need its scheduling features (calendars, complex triggers, JobDataMap).
BackgroundService (built-in)
ASP.NET Core’s BackgroundService lets you write long-running background tasks. Combine with a Timer or polling loop for simple scheduling:
public class HeartbeatService : BackgroundService
{
private readonly ILogger<HeartbeatService> _logger;
public HeartbeatService(ILogger<HeartbeatService> logger) => _logger = logger;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
while (!stoppingToken.IsCancellationRequested
&& await timer.WaitForNextTickAsync(stoppingToken))
{
try
{
_logger.LogInformation("Heartbeat: {Time}", DateTime.UtcNow);
await DoWorkAsync(stoppingToken);
}
catch (OperationCanceledException) { break; }
catch (Exception ex)
{
_logger.LogError(ex, "Heartbeat failed");
}
}
}
private Task DoWorkAsync(CancellationToken ct) => Task.CompletedTask;
}
Register in Program.cs:
builder.Services.AddHostedService<HeartbeatService>();
For cron-style scheduling (not just intervals), combine with NCrontab:
using NCrontab;
public class CronService : BackgroundService
{
private readonly CrontabSchedule _schedule = CrontabSchedule.Parse("0 9 * * 1-5");
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var next = _schedule.GetNextOccurrence(DateTime.UtcNow);
var delay = next - DateTime.UtcNow;
if (delay > TimeSpan.Zero) await Task.Delay(delay, stoppingToken);
await DoWorkAsync(stoppingToken);
}
}
private Task DoWorkAsync(CancellationToken ct) => Task.CompletedTask;
}
This is the simplest pattern but has obvious limits: in-memory only (no restart resilience), single-instance only (every replica fires every job), no retry policy, no observability beyond logs.
NCrontab
The standard .NET library for parsing cron expressions.
dotnet add package NCrontab
using NCrontab;
var schedule = CrontabSchedule.Parse("*/5 * * * *");
// Next 10 occurrences
var occurrences = schedule.GetNextOccurrences(DateTime.UtcNow, DateTime.UtcNow.AddDays(1));
foreach (var o in occurrences) Console.WriteLine(o);
// Validate user input
if (CrontabSchedule.TryParse(userInput, out var parsed))
{
// safe to use
}
// 6-field with seconds (opt-in)
var withSeconds = CrontabSchedule.Parse("*/30 * * * * *",
new CrontabSchedule.ParseOptions { IncludingSeconds = true });
Accepts standard 5-field Unix cron by default; pass IncludingSeconds = true for 6-field. Use NCrontab when you need parsing + next-runs but don’t want to pull in Quartz.NET or Hangfire.
Quartz.NET
A direct port of Java’s Quartz Scheduler — same concepts (JobDetail, Trigger, Scheduler), same expression syntax. See the Quartz dialect reference for the cron syntax.
dotnet add package Quartz.AspNetCore
Register and define jobs in Program.cs:
builder.Services.AddQuartz(q =>
{
var jobKey = new JobKey("DailyReport");
q.AddJob<DailyReportJob>(opts => opts.WithIdentity(jobKey));
q.AddTrigger(opts => opts
.ForJob(jobKey)
.WithIdentity("DailyReport-trigger")
.WithCronSchedule("0 0 9 ? * MON-FRI"));
});
builder.Services.AddQuartzHostedService(opts =>
{
opts.WaitForJobsToComplete = true;
});
Define the job:
public class DailyReportJob : IJob
{
public async Task Execute(IJobExecutionContext context)
{
// ... do work
}
}
What Quartz.NET adds beyond BackgroundService:
- Quartz cron syntax — 6 or 7 fields with seconds and optional year, plus
?,L,W,#for advanced scheduling (last day of month, nearest weekday, nth weekday) - JobDataMap — pass parameters into jobs
- Persistent job stores —
AdoJobStore(persists to a database via ADO.NET; the internal implementation isJobStoreTX) or the in-memoryRAMJobStore. AdoJobStore survives restarts. (There’s no “JDBC” store — that’s the Java edition; Quartz.NET uses ADO.NET.) - Clustering — multiple Quartz.NET instances coordinate via the shared job store
- Calendar support — exclude holidays, weekends, etc.
- Misfire handling — what to do when a fire was missed (run-now, skip, etc.)
Quartz.NET is the heavyweight option. If you’re coming from Java Quartz, it’ll feel familiar. If you’re not, the API is verbose for what you usually need — Hangfire is often a friendlier fit for .NET-native devs.
Hangfire
A background job queue with cron-style scheduling and a built-in web dashboard. The most popular scheduling library for production ASP.NET Core apps.
dotnet add package Hangfire.AspNetCore
dotnet add package Hangfire.SqlServer # or .Redis / .PostgreSql
Configure in Program.cs:
builder.Services.AddHangfire(config =>
config.UseSqlServerStorage(connectionString));
builder.Services.AddHangfireServer();
var app = builder.Build();
app.UseHangfireDashboard("/hangfire"); // web UI at /hangfire
Schedule a recurring job:
RecurringJob.AddOrUpdate(
"daily-report",
() => Console.WriteLine("Running daily report"),
"0 9 * * MON-FRI",
new RecurringJobOptions { TimeZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time") });
// Or with a service injected
RecurringJob.AddOrUpdate<ReportService>(
"daily-report",
svc => svc.GenerateDailyAsync(),
"0 9 * * MON-FRI");
What Hangfire does that Quartz.NET also does, but more ergonomically:
- Persistent jobs — stored in SQL Server, PostgreSQL, or Redis
- Distributed by default — multiple Hangfire servers share the same storage
- Retry policies — automatic exponential backoff on failure
- Built-in dashboard — /hangfire URL shows queued, scheduled, processing, succeeded, failed jobs
- Continuations — chain jobs that fire when their parent completes
Hangfire’s appeal is the dashboard plus a friendlier cron syntax: it parses recurring-job expressions with Cronos (not NCrontab), which accepts standard 5-field cron — no Quartz ? required — and adds an optional leading seconds field and the L/W/# extensions when you need them. For ASP.NET Core apps that don’t need Quartz’s calendar/misfire/JobDataMap, Hangfire wins on ergonomics.
Choosing the right one
- Simple intervals in ASP.NET Core? → BackgroundService + PeriodicTimer
- Need cron syntax but stay light? → BackgroundService + NCrontab
- Need durable scheduled jobs + retry + dashboard? → Hangfire
- Need advanced scheduling (calendars, misfire policies, nth-weekday)? → Quartz.NET
- Coming from Java Quartz? → Quartz.NET (familiar API)
- Need SLA-grade reliability? → Move scheduling out of .NET. Use Kubernetes CronJob or AWS EventBridge Scheduler to invoke an ASP.NET Core HTTP endpoint on a schedule.
For most production ASP.NET Core apps, Hangfire is the default answer — it covers 90% of scheduling needs with operational ergonomics that BackgroundService and Quartz.NET don’t match. Reach for Quartz.NET when you need its specific advanced features.
Frequently asked questions
Which .NET scheduling library should I use?
Does ASP.NET Core have built-in cron support?
What's the difference between Quartz.NET and Hangfire?
Can I run Hangfire jobs across multiple servers?
Does NCrontab actually run jobs?
Related
Every 5 Minutes
`*/5 * * * *` — common .NET scheduled-job interval.
ToolCron Parser
Verify cron expressions before adding them to Quartz.NET or Hangfire.
GuideJava Quartz Scheduler
Quartz.NET ports the Java Quartz API closely — the Java guide explains the model.
GuideNode.js Cron Jobs
The Node equivalents.
GuideKubernetes CronJob
Run scheduled .NET work as a K8s CronJob for SLA-grade reliability.