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.
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.
Here is why people choose Quartz.NET over a hand-written timer:
| You want this | Plain timer | Quartz.NET |
|---|---|---|
| Run every 5 minutes | Possible, but clumsy | Easy, one line |
| Run "every Monday 9 AM" | Very hard | Easy with cron |
| Survive an app restart | No, the plan is lost | Yes, with a database |
| One run across many servers | No, it runs everywhere | Yes, clustering |
| Pause and resume a job | You build it yourself | Built in |
The three big ideas
Quartz.NET has only three core pieces. Once you understand these three words, the rest is just typing.
- Job - the work to do. It is a class that does one task, like "send the email".
- Trigger - the schedule. It tells Quartz when to run the job.
- Scheduler - the brain. It watches the clock and matches triggers to jobs.
The three Quartz pieces working together
Steps
Job
What to do
Trigger
When to do it
Scheduler
Watches the clock
Run
Job fires 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.HostingThe 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.
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 expression | Meaning |
|---|---|
0 0/5 * * * ? | Every 5 minutes |
0 0 9 * * ? | Every day at 9:00 AM |
0 0 9 ? * MON-FRI | Weekdays at 9:00 AM |
0 0 0 1 * ? | Midnight on the 1st of each month |
0 30 22 ? * SAT | Saturdays 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 FriThe ? 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
Steps
Fixed interval
Use SimpleSchedule
Calendar time
Use CronSchedule
One-time run
Use a start-at time
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.
The table below helps you decide which store to pick for your situation.
| Question | RAM store | Database store |
|---|---|---|
| Easiest to set up? | Yes | Needs tables |
| Survives a restart? | No | Yes |
| Works across many servers? | No | Yes (clustering) |
| Best for learning? | Yes | Later |
| Best for production? | Rarely | Usually |
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
Steps
Trigger fires
All servers see it
Lock taken
One server wins
Job runs once
Others skip
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
IJoband has oneExecutemethod. - 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
- Quartz.NET ASP.NET Core Integration (Official Docs)
- Quartz.NET Jobs and Triggers Tutorial
- Quartz.NET Configuration Reference
- Andrew Lock: Using Quartz.NET with ASP.NET Core and worker services
- Milan Jovanovic: Scheduling Background Jobs With Quartz.NET
- Anton DevTips: Scheduling Jobs With Quartz and EF Core Database Persistence
Related Posts
Adding Real-Time Functionality to .NET Apps with SignalR
Learn ASP.NET Core SignalR step by step: hubs, clients, groups, and scaling with Redis or Azure, explained for absolute beginners.
TickerQ: The Modern .NET Job Scheduler That Beats Quartz and Hangfire
Learn TickerQ, the fast, reflection-free .NET job scheduler with cron and time jobs, EF Core storage, retries, and a live dashboard, explained for beginners.
Improving ASP.NET Core Dependency Injection with Scrutor
Learn how Scrutor makes ASP.NET Core dependency injection easier with assembly scanning and decoration, explained in simple, beginner-friendly steps.
Master Configuration in ASP.NET Core with the Options Pattern
Learn the ASP.NET Core options pattern step by step: bind appsettings, use IOptions, IOptionsSnapshot, IOptionsMonitor, and validate config safely.
How to Add JWT Authentication to SignalR Hubs in ASP.NET Core
A beginner-friendly guide to securing SignalR hubs with JWT tokens in ASP.NET Core, including the access_token query string trick and the [Authorize] attribute.
Vertical Slice Architecture: How to Structure Your Slices in .NET
Learn vertical slice architecture in .NET with a simple tiffin-box analogy, feature folders, CQRS, code examples, diagrams, and clear structuring rules.