Skip to main content
SEMastery
ASP.NETbeginner

Scheduling Background Jobs with Quartz.NET in ASP.NET Core

Learn Quartz.NET step by step in ASP.NET Core: jobs, triggers, cron schedules, dependency injection, and database persistence, explained for beginners.

12 min readUpdated April 13, 2026

Imagine your mother sets a daily alarm to water the tulsi plant every morning at 6 AM. She does not stand by the clock all day waiting. She trusts the alarm. When the time comes, it rings, she waters the plant, and life goes on. The alarm remembers the schedule even if she forgets.

A web app needs the same kind of helper. Some work should happen on a schedule, not when a user clicks a button. Sending a daily report email. Cleaning up old files at midnight. Checking for new orders every 5 minutes. You do not want a person sitting there triggering these jobs by hand.

Quartz.NET is that smart alarm clock for your .NET app. It runs jobs at the times you choose, again and again, quietly in the background. In this guide we will learn what it is, how it works, and how to wire it into ASP.NET Core. We will keep the sentences short and the steps small.

What problem does Quartz.NET solve?

Think about a school bell. A peon does not watch the clock for the whole day. The school sets a timetable once. The bell rings at the right times: assembly, first period, lunch, home time. The timetable is the plan. The bell is the doer.

Quartz.NET gives your app this exact split. You write a plan (when to run) and a job (what to do). Quartz watches the time and fires the job for you. You never write a messy loop full of Thread.Sleep. You just describe the schedule.

A school timetable is like a Quartz schedule

Here is why people choose Quartz.NET over a hand-written timer:

You want thisPlain timerQuartz.NET
Run every 5 minutesPossible, but clumsyEasy, one line
Run "every Monday 9 AM"Very hardEasy with cron
Survive an app restartNo, the plan is lostYes, with a database
One run across many serversNo, it runs everywhereYes, clustering
Pause and resume a jobYou build it yourselfBuilt in

The three big ideas

Quartz.NET has only three core pieces. Once you understand these three words, the rest is just typing.

  1. Job - the work to do. It is a class that does one task, like "send the email".
  2. Trigger - the schedule. It tells Quartz when to run the job.
  3. Scheduler - the brain. It watches the clock and matches triggers to jobs.

The three Quartz pieces working together

Job
Trigger
Scheduler
Run

Steps

1

Job

What to do

2

Trigger

When to do it

3

Scheduler

Watches the clock

4

Run

Job fires on time

The scheduler connects a job to its trigger and fires it on time.

A helpful way to remember it: the job is the recipe, the trigger is the meal time, and the scheduler is the cook who looks at the clock and starts cooking at the right moment.

Step 1: Install the package

First, add Quartz.NET to your ASP.NET Core project. Open a terminal in your project folder and run the command below. The second package wires Quartz into the app's start and stop, so you do not have to start the scheduler by hand.

dotnet add package Quartz
dotnet add package Quartz.Extensions.Hosting

The Quartz package is the engine. The Quartz.Extensions.Hosting package is the glue that connects the engine to ASP.NET Core. Together they let your jobs start when the app starts and stop cleanly when the app stops.

Step 2: Write your first job

A job is just a class that implements the IJob interface. That interface asks for one method: Execute. Whatever you put inside Execute is the work that runs on schedule.

Let us make a tiny job that prints a friendly message. In real life this could send an email or clean a database, but printing keeps the idea clear.

using Quartz;
 
public class GreetingJob : IJob
{
    private readonly ILogger<GreetingJob> _logger;
 
    // Quartz can inject your normal services here.
    public GreetingJob(ILogger<GreetingJob> logger)
    {
        _logger = logger;
    }
 
    public async Task Execute(IJobExecutionContext context)
    {
        _logger.LogInformation(
            "Hello! The job ran at {Time}", DateTimeOffset.Now);
 
        // Pretend to do some real work.
        await Task.CompletedTask;
    }
}

Notice two nice things. The Execute method returns a Task, so your jobs can be fully async. And the constructor takes an ILogger, which means Quartz works with the normal ASP.NET Core dependency injection you already know.

Step 3: Register Quartz and schedule the job

Now we tell ASP.NET Core about Quartz inside Program.cs. We do three things: add Quartz, register our job with a trigger, and add the hosted service that runs everything.

using Quartz;
 
var builder = WebApplication.CreateBuilder(args);
 
builder.Services.AddQuartz(q =>
{
    // Give the job a name (a "key").
    var jobKey = new JobKey("GreetingJob");
 
    q.AddJob<GreetingJob>(opts => opts.WithIdentity(jobKey));
 
    // Add a trigger that runs the job every 10 seconds.
    q.AddTrigger(opts => opts
        .ForJob(jobKey)
        .WithIdentity("GreetingJob-trigger")
        .WithSimpleSchedule(s => s
            .WithIntervalInSeconds(10)
            .RepeatForever()));
});
 
// This starts and stops the scheduler with the app.
builder.Services.AddQuartzHostedService(opts =>
{
    opts.WaitForJobsToComplete = true;
});
 
var app = builder.Build();
app.Run();

Run the app and watch the console. Every 10 seconds you will see the "Hello!" line appear. You just built a working background scheduler. The WaitForJobsToComplete = true line is a kind setting: when you stop the app, Quartz lets a running job finish instead of cutting it off in the middle.

How a registered job flows from app start to running

Simple triggers vs cron triggers

You saw a simple trigger above. It is perfect for "every X seconds or minutes". But what about "every weekday at 9 AM"? For calendar-style timing, Quartz gives you the cron trigger.

A cron expression is a short pattern of fields that describes time. It looks strange at first, but it reads left to right: seconds, minutes, hours, day-of-month, month, day-of-week.

Cron expressionMeaning
0 0/5 * * * ?Every 5 minutes
0 0 9 * * ?Every day at 9:00 AM
0 0 9 ? * MON-FRIWeekdays at 9:00 AM
0 0 0 1 * ?Midnight on the 1st of each month
0 30 22 ? * SATSaturdays at 10:30 PM

Here is the same greeting job, but now firing on a cron schedule instead of a fixed interval.

q.AddTrigger(opts => opts
    .ForJob(jobKey)
    .WithIdentity("GreetingJob-cron")
    .WithCronSchedule("0 0 9 ? * MON-FRI")); // 9 AM, Mon to Fri

The ? you see in some fields means "no specific value". It is used in the day-of-month or day-of-week spot because you cannot set both at the same time. When in doubt, build and test your cron pattern with an online cron helper before shipping it.

Choosing the right trigger

Need a schedule?
Fixed interval?
Calendar time?

Steps

1

Fixed interval

Use SimpleSchedule

2

Calendar time

Use CronSchedule

3

One-time run

Use a start-at time

Pick a simple trigger for intervals and a cron trigger for calendar times.

Dependency injection: using your own services

Real jobs rarely just print text. They talk to a database, call an API, or send an email. The good news is that Quartz jobs are scoped services. That means you can inject any service you registered, exactly like you do in a controller.

Say you have an IEmailSender service. Your job can ask for it in the constructor, and Quartz will hand over a fresh, correctly-scoped copy every time the job runs.

public class DailyReportJob : IJob
{
    private readonly IEmailSender _email;
    private readonly IReportBuilder _reports;
 
    public DailyReportJob(IEmailSender email, IReportBuilder reports)
    {
        _email = email;
        _reports = reports;
    }
 
    public async Task Execute(IJobExecutionContext context)
    {
        var report = await _reports.BuildDailySummaryAsync();
        await _email.SendAsync("[email protected]", "Daily report", report);
    }
}

Because each run gets its own scope, you can safely use a scoped DbContext from Entity Framework Core inside a job without the leaks you might fear. Quartz creates the scope, runs the job, and disposes the scope when the job ends.

Step 4: Make jobs survive a restart

By default, Quartz keeps all schedules in memory. This is called the RAMJobStore. It is fast, but it has one weakness: if your app restarts, the memory is wiped and Quartz forgets everything. For a hobby project that is fine. For a real system, you usually want the plan to live in a database so it survives restarts and crashes.

Quartz can save jobs and triggers to a SQL database using its ADO.NET store (often called AdoJobStore). You point Quartz at a connection string, run the Quartz table scripts once, and now your schedules are durable.

builder.Services.AddQuartz(q =>
{
    q.UsePersistentStore(store =>
    {
        store.UseProperties = true;
        store.UseSqlServer(connectionString);
        store.UseSystemTextJsonSerializer();
    });
 
    // ... add your jobs and triggers as before
});

The table scripts that create the Quartz tables ship with the project on GitHub. You run them once against your database, the same way you would run any migration. After that, every job and trigger is written to those tables.

RAM store versus database store on restart

The table below helps you decide which store to pick for your situation.

QuestionRAM storeDatabase store
Easiest to set up?YesNeeds tables
Survives a restart?NoYes
Works across many servers?NoYes (clustering)
Best for learning?YesLater
Best for production?RarelyUsually

Running safely across many servers

Big apps often run on more than one machine at the same time. This is great for handling lots of users, but it creates a tricky question for jobs. If your "send daily report" job is set up on three servers, will the boss get three emails at 9 AM?

With the database store turned on, Quartz can run in clustering mode. The servers share one database. When the time comes, they all see the trigger, but only one server wins the right to run the job. The others step back. So the report is sent exactly once.

Clustering: one job, many servers, single run

Server A
Server B
Server C
Shared DB

Steps

1

Trigger fires

All servers see it

2

Lock taken

One server wins

3

Job runs once

Others skip

All servers share a database; only one runs each job instance.

To turn on clustering, you add a couple of settings to the persistent store. The key one is telling Quartz that the store is clustered, and giving each running copy the same scheduler name.

q.UsePersistentStore(store =>
{
    store.UseClustering();          // turn on cluster mode
    store.UseSqlServer(connectionString);
    store.UseSystemTextJsonSerializer();
});

Quartz.NET vs a plain BackgroundService

ASP.NET Core already has a built-in tool called BackgroundService. So when do you need Quartz at all? Think of it like the difference between a kitchen timer and a full calendar app. The timer is great for "ring in 20 minutes". The calendar app is for "every second Tuesday, plus reminders, that I can edit later".

  • Use BackgroundService when you want a simple loop that runs while the app is alive and the timing is loose.
  • Use Quartz.NET when you need real calendar schedules, durable plans that survive restarts, pause and resume, retries, or one-run-across-many-servers.

A quick word on the wider .NET ecosystem: some popular libraries that people once reached for, like MediatR and MassTransit, have moved to commercial licensing. Quartz.NET is different. Its core scheduling library stays free and open source under the Apache 2.0 license, which is one reason it remains a safe, friendly default for background scheduling.

A few good habits

As you build real jobs, keep these gentle rules in mind. They will save you from late-night surprises.

  • Keep jobs short and focused. One job, one task. Easy to test, easy to fix.
  • Make jobs safe to repeat. If a job runs twice by accident, it should not cause damage. This idea is called being idempotent.
  • Log the start and end. A simple log line tells you whether a job ran, and how long it took.
  • Handle errors inside the job. If your job throws, catch it, log it, and decide whether to retry. Do not let one failure crash everything.
  • Test cron expressions before shipping. A wrong character can mean a job that runs every second instead of every day.

Quick recap

  • Quartz.NET is a smart alarm clock for your .NET app. It runs jobs on a schedule.
  • The three core ideas are Job (what to do), Trigger (when to do it), and Scheduler (the brain that watches the clock).
  • A job is a class that implements IJob and has one Execute method.
  • Simple triggers are for fixed intervals like "every 10 seconds". Cron triggers are for calendar times like "weekdays at 9 AM".
  • Jobs are scoped services, so you can inject your own services like an email sender or DbContext.
  • The default RAM store forgets everything on restart. Use the database store to make schedules durable.
  • Clustering lets many servers share one database so a job runs exactly once.
  • Quartz.NET's core library is free and open source, unlike some other tools that recently moved to paid licensing.

References and further reading

Related Posts