Quartz.NETQuartz.NET
Home
Features
Blog
Discussions
NuGet
GitHub
Home
Features
Blog
Discussions
NuGet
GitHub
  • Getting Started

    • Overview
    • Quartz 4 Quick Start
    • Tutorial
      • Using Quartz
      • Jobs And Triggers
      • More About Jobs & JobDetails
      • Job Data
      • More About Triggers
      • Querying Jobs and Triggers
      • Simple Triggers
      • Cron Triggers
      • RecurrenceTrigger
      • Time and TimeProvider
      • Trigger and Job Listeners
      • Scheduler Listeners
      • Job Execution Middleware
      • Job Stores
      • Configuration, Resource Usage and Building a Scheduler
      • Building a Scheduler Without a Host
      • Clustering
      • Execution Groups
      • Node Affinity (Preferred Node)
      • Testing
      • Compile-Time Checks
      • Declaring Jobs with Attributes
      • Delegate Jobs
    • Configuration Reference
    • JSON Configuration
    • Cron Expression Reference
    • Multi-Tenancy
    • Comparison
    • Frequently Asked Questions
    • Best Practices
    • Before You Go Live
    • Operating a Cluster
    • Log Events
    • Tenancy Patterns
    • Database Schema
    • Database Schema Changes
    • Migration Guide
    • Troubleshooting
    • API Documentation
  • How To's
    • One-Off Job
    • Rescheduling Jobs
    • Retrying Failed Jobs
    • Job Continuations
    • Progress and Execution Logs
    • Multiple Triggers
    • Job Template
    • Running Quartz under Aspire
    • Quartz.NET with Wolverine
    • Coming from Hangfire
    • Coming from TickerQ
    • Embedding Quartz in a Library
    • Running under an External Leader Election
    • Publishing Trimmed and Native AOT
    • Extending Quartz: what is open, what is closed, and how to ask
    • A Job Store of Your Own
    • A Driver Delegate for a New Database
    • Persisting a Custom Trigger Type
    • A Lock Handler of Your Own
  • Packages

    • Quartz Core Additions

      • Jobs
      • Serialization (System.Text.Json)
      • JSON Serialization
      • Plugins
    • Integrations

      • Aspire Integration
      • ASP.NET Core Integration
      • HTTP API
      • HTTP Client
      • Dashboard
      • Hosted Services Integration
      • Microsoft DI Integration
      • Multiple Schedulers with Microsoft DI
      • Observability
      • Redis Lock Handler
      • TimeZoneConverter Integration
    • 3rd Party Plugins for Quartz
  • Quartz 3.x

    • Getting Started

      • Quartz 3 Quick Start
      • Tutorial
        • Using Quartz
        • Library Overview
        • Jobs And Triggers
        • More About Jobs
        • More About Triggers
        • Execution Groups
        • Node Affinity (Preferred Node)
        • Simple Triggers
        • Cron Triggers
        • RecurrenceTrigger
        • Trigger and Job Listeners
        • Scheduler Listeners
        • Job Stores
        • Tuning the Scheduler
        • Configuration, Resource Usage and SchedulerFactory
        • Advanced (Enterprise) Features
      • Configuration Reference
      • JSON Configuration
      • Multi-Tenancy
      • Frequently Asked Questions
      • Best Practices
      • Tenancy Patterns
      • Troubleshooting
      • API Documentation
      • Database Schema
      • Database Schema Changes
      • Migration Guide
      • Miscellaneous Features
    • How To's

      • One-Off Job
      • Multiple Triggers
      • Job Template
      • Using the CronTrigger
      • Rescheduling Jobs
    • Packages

      • Quartz Core Additions

        • Dashboard
        • Jobs
        • Serialization (System.Text.Json)
        • Serialization (Newtonsoft Json.NET)
        • Plugins
      • Integrations

        • ASP.NET Core Integration
        • Hosted Services Integration
        • Microsoft DI Integration
        • Multiple Schedulers with Microsoft DI
        • OpenTelemetry Integration
        • OpenTracing Integration
        • Redis Lock Handler
        • TimeZoneConverter Integration
      • 3rd Party Plugins for Quartz
  • Old Releases

    • Quartz 2.x
      • Quartz 2 Quick Start
      • Tutorial
        • Lesson 1: Using Quartz
        • Lesson 2: Jobs And Triggers
        • Lesson 3: More About Jobs & JobDetails
        • Lesson 4: More About Triggers
        • Lesson 5: SimpleTrigger
        • Lesson 6: CronTrigger
        • Lesson 7: TriggerListeners and JobListeners
        • Lesson 8: SchedulerListeners
        • Lesson 9: JobStores
        • Lesson 10: Configuration, Resource Usage and SchedulerFactory
        • Lesson 11: Advanced (Enterprise) Features
        • Lesson 12: Miscellaneous Features of Quartz
        • CronTrigger Tutorial
      • Configuration Reference
      • Migration Guide
      • API Documentation
    • Quartz 1.x
      • Tutorial
        • Lesson 1: Using Quartz
        • Lesson 2: Jobs And Triggers
        • Lesson 3: More About Jobs & JobDetails
        • Lesson 4: More About Triggers
        • Lesson 5: SimpleTrigger
        • Lesson 6: CronTrigger
        • Lesson 7: TriggerListeners and JobListeners
        • Lesson 8: SchedulerListeners
        • Lesson 9: JobStores
        • Lesson 10: Configuration, Resource Usage and SchedulerFactory
        • Lesson 11: Advanced (Enterprise) Features
        • Lesson 12: Miscellaneous Features of Quartz
      • API Documentation
  • License

Progress and Execution Logs

A running job can say how far it has got, and what it logs while it runs can be kept with its history row. Progress shows on the dashboard's Currently Executing page, on every node of a cluster; the log shows on the execution's own page, reached from Execution History.

Report progress

Call IJobExecutionContext.ReportProgress(percent, message) as often as is convenient:

public sealed class ExportJob : IJob
{
    public async ValueTask Execute(IJobExecutionContext context, CancellationToken cancellationToken = default)
    {
        const int pages = 40;

        for (int page = 1; page <= pages; page++)
        {
            await ExportPage(page, cancellationToken);

            // Returns at once. The scheduler writes at most once a second, and only a change.
            context.ReportProgress(page * 100 / pages, $"page {page} of {pages}");
        }
    }

    private static ValueTask ExportPage(int page, CancellationToken cancellationToken) => default;
}
RuleValue
percent0 to 100; anything else throws ArgumentOutOfRangeException
messageOptional; cut to 250 characters (FireInstanceProgress.MaxMessageLength)
Store writesAt most one per second per firing, only when the value changed
The last reportAlways written, however quickly the reports came
The job's threadNever waits: the write is queued off the job's flow and enlists in nothing
A failed writeLogged as event 1058, and the job carries on
LifetimeThe firing's: gone when the job completes; a retry starts with none

Read it

FireInstance.Progress and FireInstance.ProgressMessage, from QueryFireInstances:

PagedResult<FireInstance> running = await scheduler.QueryFireInstances(new FireInstanceQuery());

foreach (FireInstance firing in running.Items)
{
    // Null until the job reports; cluster-wide with a persistent store.
    Console.WriteLine($"{firing.JobKey}: {firing.Progress}% {firing.ProgressMessage}");
}
  • Both are null until the job reports, and on a firing that is only Acquired.
  • The persistent store keeps them in QRTZ_FIRED_TRIGGERS.PROGRESS and PROGRESS_MESSAGE, so every node, and a store-attached dashboard, reads them. Run 4.3/add_fire_progress_<db>.sql first.
  • Over HTTP, GET …/jobs/fire-instances carries progress and progressMessage.
  • The dashboard draws a bar on Currently Executing.

Keep a job's log lines

UseExecutionLogCapture() keeps what each of the scheduler's jobs logs while it runs, and records it on the execution's history row:

builder.Services.AddQuartz(q =>
{
    // First, so what later middleware logs is kept too. The bounds are the defaults.
    q.UseExecutionLogCapture(options =>
    {
        options.MaxLines = 200;
        options.MaxBytes = 16 * 1024;
    });

    q.AddJob<ImportJob>(j => j.WithIdentity("import", "nightly"));
});

The job logs as it always did:

public sealed class ImportJob : IJob
{
    private readonly ILogger<ImportJob> logger;

    public ImportJob(ILogger<ImportJob> logger)
    {
        this.logger = logger;
    }

    public ValueTask Execute(IJobExecutionContext context, CancellationToken cancellationToken = default)
    {
        // An ordinary ILogger: the line goes wherever the host sends logs, and onto this firing's
        // history row as well.
        logger.LogInformation("Importing {File}", context.MergedJobDataMap.GetString("file"));
        return default;
    }
}
RuleValue
What is keptLines logged through the container's ILoggerFactory on the firing's flow
WhenFrom the job's start to the firing's end, its unhandled exception included
What is not keptLines logged outside a firing, or in another firing
BoundsMaxLines (200) and MaxBytes (16 KB, UTF-8) per execution; the oldest line goes first
When a line was droppedThe log opens with [n earlier log entries dropped: …]
Line format2026-09-26T12:00:00.123Z info Category: message, UTC, then the exception if any
FilteringAs any logging provider; its alias is QuartzExecutionLog
Other schedulersUnaffected: capture is the calling scheduler's choice
Cost without itNone: no provider is registered, so no log call reaches it
Invalid boundsMaxLines < 1 or MaxBytes < 256: SchedulerConfigException at build

Read a log

ExecutionHistoryEntry.Log, from IExecutionHistoryStore.GetExecution(schedulerName, entryId):

PagedResult<ExecutionHistoryEntry> page = await history.QueryExecutions(
    new ExecutionHistoryQuery { SchedulerName = schedulerName, Take = 10 });

foreach (ExecutionHistoryEntry row in page.Items)
{
    // The listing may leave the log out; the single read always carries it.
    ExecutionHistoryEntry? execution = await history.GetExecution(schedulerName, row.EntryId!);
    Console.WriteLine(execution?.Log ?? "(nothing captured)");
}
WhereCarries the log
GetExecutionAlways
QueryExecutions, in-memory historyYes
QueryExecutions, database historyNo: EXECUTION_LOG is not in the listing's SELECT
GET …/history/executions/{entryId}Yes, as log
GET …/history/executionsNo; each row carries entryId
DashboardThe execution's page, from a History row's fire time
  • The database history needs 4.3/add_execution_log_<db>.sql; a store that keeps its history there refuses to start without it.
  • The in-memory history keeps each log in memory, so its size is bounded by MaxEntriesPerScheduler × MaxBytes.

A job store of your own

IJobStore.UpdateFireInstanceProgress(fireInstanceId, progress) is a default interface member that records nothing, so a store written for 4.2 compiles and its firings report no progress. Implement it to keep the value beside the firing, and return it on FireInstance.Progress and ProgressMessage. A fire instance that has completed is not an error: update nothing.

A history store of your own

IExecutionHistoryStore.GetExecution is a default interface member. It reads the scheduler's whole history through QueryExecutions and picks the row out by EntryId, so a store written for 4.2 answers it. Override it with a read by key. Keep EntryId and Log on the rows you store; the recorder sets both.

Help us by improving this page!
Last Updated: 9/26/26, 7:42 PM
Contributors: Marko Lahma, Claude Opus 5.5
Prev
Job Continuations
Next
Multiple Triggers