cronuru
Guide

.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

LibraryWhat it doesBest for
BackgroundService + TimerDIY scheduling in ASP.NET CoreSimple intervals, single-process apps
NCrontabCron expression parsing + next-runAdding cron syntax to a custom scheduler
CronosModern alternative to NCrontabSame use case; some prefer the API
Quartz.NETFull-featured scheduler ported from Java QuartzComplex schedules, persistence, clustering
HangfireBackground job queue with cron scheduling + dashboardProduction apps wanting durability + observability
CoravelLightweight scheduler + queue for ASP.NET CoreSmaller 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 storesAdoJobStore (persists to a database via ADO.NET; the internal implementation is JobStoreTX) or the in-memory RAMJobStore. 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?
For simple needs in ASP.NET Core: BackgroundService + Timer or NCrontab. For complex schedules with persistence and clustering: Quartz.NET. For job queue + cron + a dashboard: Hangfire. Most ASP.NET Core apps start with BackgroundService and graduate to Hangfire when they need durability.
Does ASP.NET Core have built-in cron support?
There's no first-class cron API, but you can combine IHostedService (or BackgroundService) with a Timer or a cron expression parser like NCrontab to roll your own. Many tutorials show this pattern as 'the built-in way'. For complex scheduling you'll outgrow it quickly — that's when Quartz.NET or Hangfire enters the picture.
What's the difference between Quartz.NET and Hangfire?
Quartz.NET is a scheduler — it fires jobs at scheduled times. Hangfire is a job queue that happens to include cron scheduling — it persists jobs, retries failures, and includes a web dashboard for observability. For pure scheduling, Quartz.NET. For 'I need scheduled jobs AND background queue AND dashboard', Hangfire.
Can I run Hangfire jobs across multiple servers?
Yes. Hangfire stores jobs in shared storage (SQL Server, Redis, PostgreSQL). Multiple Hangfire servers pulling from the same storage coordinate automatically — scheduled jobs fire once, even with N server instances. Configure storage via `app.UseHangfireServer()` in Startup.
Does NCrontab actually run jobs?
No. NCrontab parses cron expressions and computes next-run times. It doesn't have a scheduler — you have to combine it with your own scheduling loop (a Timer in a BackgroundService, or a long-running while/sleep). Use NCrontab when you want minimal dependencies and don't need a full scheduling framework.