Monitoring .NET scheduled tasks - Tools and alternatives

TOC

There's a lot of possibilities when it comes to developing scheduled tasks with .NET. This post is a guide to monitoring scheduled tasks in a range of different setups and frameworks. Most developers have some form of error logging in place but often forget about the most important thing when monitoring scheduled tasks. Stay tuned to learn more.

Before we start digging into the advanced possibilities for monitoring Windows Scheduled Tasks, I want to introduce you to the features already built into Windows. In some cases, this may be sufficient to implement a monitoring strategy. If running mission-critical features on Scheduled Tasks, you probably don't want to miss out on some of the more advanced tools introduced later in this post, though.

As you already know if you are familiar with Windows Scheduled Tasks, everything can be managed from within the Task Scheduler application that comes as part of Windows. Simply click the (Windows) button and type task scheduler.

Task Scheduler is a pretty complex tool, offering a lot of options for running one or more executables in different schedules. There are a lot of good guides out there that show you how to schedule tasks, why I don't want to repeat that in this post. The main focus here is monitoring. Let's take a look at how my local Task Scheduler look (after selecting Task Scheduler Library to see all tasks):

As shown on the screenshot, my machine has a long list of scheduled tasks to execute. Some every day, others every hour, etc. The most important columns to look at are Last Run Time and Last Run Result. In case you have a task that needs to run daily, you will need to launch Task Scheduler daily to verify that:

  1. The task ran since you check yesterday (by looking at Last Run Time).
  2. The task succeeded (by looking at Last Run Result).

Here's an enlarged example of those two columns:

By inspecting these two columns, you get a pretty good overview of your tasks and if they ran or not. In case of an executable throwing an error or returning a fault status code to Windows, The Last Run Result column will show the returned error.

If you don't want to monitor tasks EVERY time they are supposed to have run, you can enable task history from within the Task Scheduler. This is done by clicking the Enable All Tasks History action in the Actions window:

As suggested by the name, this is a global action that will store a history of ALL scheduled tasks. To my knowledge, you cannot enable a history for individual tasks only, which would be a nice feature to have from time to time.

The run history is available on the History tab when clicking a scheduled task:

Enabling task history simply instruct Task Scheduler to log messages to the Windows Event Log why these messages can be found through the event log as well.

Enabling task history simply instruct Task Scheduler to log messages to the Windows Event Log why these messages can be found through the event log as well. Click the button again, type Event Viewer, and launch the application. Log messages from Task Scheduler can be found beneath Event Viewer (local) > Applications and Services Logs > Microsoft > Windows > TaskScheduler > Operational:

The advantage of looking at the logs through the Event Viewer is that you get an overview of all executed tasks. The downside is that, while Event Viewer offers some limited filter capabilities, you quickly lose track of the log messages.

Monitoring when tasks fail

Manually monitoring scheduled tasks is just asking for trouble. We have all implemented a scheduled task to run every night only to hear someone at the standup: "The nightly batch job hasn't run the last 2 weeks". Dead silence. Who was responsible? Well no-one should be responsible for executing manual tasks like that. Let's take a look at different ways to automate the process of monitoring scheduled tasks.

Set up monitoring based on events

Like I already mentioned, scheduled tasks can log messages to the Windows Event Log when enabling the All Tasks History feature. Not everyone knows this, but you can create scheduled tasks triggered by messages in the Event Log as well. With this approach, you can set up a job triggered by error messages in one or more logs. To set it up, create a new scheduled task, select the Triggers tab, and add a new trigger. In the Begin the task dropdown select On an event. By selecting Custom you can configure exactly when to trigger the task based on both the severity of the message as well as the log. In this example, I have created a new filter that only triggers on Errors and Critical messages from the TaskScheduler application in the Operational log that I already showed you in the previous section:

All left to do now is to select which action to execute when new log messages matching this filter are added to the Event Log. An example of an action could be sending an email. Scheduled Tasks have a built-in (deprecated) email feature, but a better approach would probably be to create an executable or a script to send the actual email. Here's an overview of different email services that I have written: How to send emails from C#/.NET - The definitive tutorial.

Monitoring based on logging

If your scheduled task is based on a C# console application, you may already use a logging framework like NLog, Serilog, or Microsoft.Extensions.Logging. It is often a better choice for monitoring the output from these logging frameworks, rather than monitoring the Windows Event Log.

To show you an example of how to send an email on errors logged through a logging framework I'll use NLog, a popular logging framework for .NET. The process will be similar for other frameworks.

Start by wrapping your console app in try/catch:

class Program
{
    static void Main(string[] args)
    {
        var logger = NLog.LogManager.GetCurrentClassLogger();
        try
        {
            // Execute your scheduled task code
        }
        catch (Exception e)
        {
            logger.Error(e, "Error during scheduled task");
        }
        
        NLog.LogManager.Shutdown();
    }
}

In case of an exception in your scheduled task code, the error is logged through NLog. Emails can be sent from all logging frameworks. In the case of NLog, use the Mail target in your NLog.config file:

<target name="Mail"
        xsi:type="Mail"
        smtpServer="smpt.myserver.com"
        smtpPort="587"
        ... />

Again, this is just an example of setting up a notification from a logging framework. It could be any logging framework out there and the configured target could be one of the many supported ones (like elmah.io).

Monitoring if a task run

One of the most common problems I see with Windows Scheduled Tasks is that people often have one of the two types of monitoring mentioned above. But few verify if scheduled tasks are executed. If someone accidentally disables a task, no error message is ever stored and no email is sent. So, how can we set up monitoring that verifies that scheduled tasks run?

Logging a successful message with a logging framework

Building on the logging framework monitoring that I already showed you, we can log a successful message once the scheduled task has run:

class Program
{
    static void Main(string[] args)
    {
        var logger = NLog.LogManager.GetCurrentClassLogger();
        try
        {
            // Execute your scheduled task code
            logger.Info("Scheduled task is successful");
        }
        catch (Exception e)
        {
            logger.Error(e, "Error during scheduled task");
        }
        
        NLog.LogManager.Shutdown();
    }
}

You then need to figure out how to verify that this message is logged daily. Different logging frameworks support various options there, why you will need to go through the documentation for the logging framework you use. A manual alternative would be to send an email based on a logging filter, looking at the successful message only. This way, an email (Slack message, Teams message, etc.) could be sent once the job has run. The solution is not much better than manually logging and checking if the task ran, but at least you don't need credentials to the production server.

Monitoring heartbeats with elmah.io

There's a range of tools out there providing what is called Heartbeat Monitoring. A heartbeat is simply a ping from your application which is validated by the chosen service. You typically set up an expected schedule and let Heartbeat Monitoring send you a notification if a ping is not received in time.

In this section, I'll use the elmah.io Heartbeats feature to demonstrate, but you can use any tool that supports heartbeats.

Start by creating a new Heartbeat through the elmah.io UI:

In this process, I tell elmah.io to expect a heartbeat from the scheduled task every 24 hours. Since this is a fast job in terms of runtime, I'll set the grace period to 5 minutes. This means that elmah.io should notify you if more than 24 hours and 5 minutes have passed since the last successful heartbeat.

Install the Elmah.Io.Client NuGet package:

Install-Package Elmah.Io.Client

To have your scheduled task send a heartbeat to elmah.io, extend the code from the previous example:

class Program
{
    static void Main(string[] args)
    {
        var logId = new Guid("LOG_ID");
        var api = ElmahioAPI.Create("API_KEY");
        try
        {
            // Execute your scheduled task code
            api.Heartbeats.Healthy(logId, "HEARTBEAT_ID");
        }
        catch (Exception e)
        {
            api.Heartbeats.Unhealthy(logId, "HEARTBEAT_ID");
        }
    }
}

I have left out the logging code for simplicity but it is perfectly fine to have both. In case your code runs without any exceptions a Healthy heartbeat is logged to elmah.io. If an exception occurs an Unhealthy heartbeat is logged. In case neither is logged, elmah.io will automatically log a Missing heartbeat and notify you through one of the supported integrations or email.

Like already mentioned, elmah.io is just one of the options available for Heartbeats Monitoring.

Alternatives to Windows Scheduled Tasks

Unless you have a monitoring system like elmah.io, I'm sure you see some downsides of even running code as Windows Scheduled Tasks. The built-in monitoring features are pretty limited and often rely on manual follow up. I have dedicated this section to show you a range of alternatives to Windows Scheduled Tasks.

Hangfire

There's a great framework for implementing background processes in .NET named Hangfire. Hangfire makes it easy to implement everything from fire-and-forget type of jobs to scheduled tasks.

Once set up, scheduling a recurring job is as easy as:

RecurringJob.AddOrUpdate(() =>
{
    // ...
}, Cron.Hourly);

If you are not already familiar with Hangfire I recommend you check it out.

Azure Functions

A few years ago we ran a lot of scheduled code as Windows Scheduled tasks on virtual machines running on Azure. All of this code has now been migrated to Azure Functions. Azure Functions can run in a serverless mode, which means that you only pay for the virtual machine executing the scheduled task as long as it is running. Azure Functions is a great and cheap way to implement scheduled tasks if you are already on Azure.

Here's a small example of an Azure Function scheduled to run hourly:

public class MyScheduledFunction
{
    [FunctionName("MyScheduledFunction")]
    public async Task Run([TimerTrigger("0 0 * * * *")]TimerInfo myTimer)
    {
        // Add your scheduled code here
        
        return Task.CompletedTask;
    }
}

Hosted services in .NET Core

.NET Core has built-in support for something called hosted service. A hosted service is sort of like a Windows Service but hosted inside a .NET process (like a console application). Using a simple Timer you can create scheduled tasks using hosted services:

public class MyService : IHostedService
{
    private Timer timer;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        timer = new Timer(Execute, null, TimeSpan.Zero, TimeSpan.FromHours(1));
        return Task.CompletedTask;
    }

    private void Execute(object state)
    {
        // Run your hourly code
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        // ...
        return Task.CompletedTask;
    }
}

Adding the service is easy from either a console app or ASP.NET Core:

services.AddHostedService<MyService>();

Quartz.NET

A scheduling framework that I have used in the past is Quartz.NET. Like hosted services, Quartz.NET let you define jobs/services:

public class MyJob : IJob
{
    public Task Execute(JobExecutionContext context)
    {
        // ...
        return Task.CompletedTask;
    }
}

Jobs can then be scheduled:

var job = JobBuilder.Create<MyJob>().Build();
var trigger = TriggerBuilder.Create()
    .StartNow()
    .WithSimpleSchedule(x => x
        .WithIntervalInHours(1)
        .RepeatForever())            
    .Build();
await scheduler.scheduleJob(job, trigger);

Quartz.NET support a couple of integrations with other frameworks like ASP.NET Core and even hosted services, which make it a good alternative to simple timers.

Background workers in ASP.NET

I wanted to put a few words on background workers in ASP.NET since I often see people recommend them as alternatives for scheduled tasks. ASP.NET supports long-running tasks through the HostingEnvironment.QueueBackgroundWorkItem method. The idea behind QueueBackgroundWorkItem is to have the webserver execute a long-running task in an async way that let a controller action return a response to the client before finishing the task. QueueBackgroundWorkItem is not suited for scheduling tasks.

Features steps
We monitor your websites

We monitor your websites

We monitor your websites for crashes and availability. This helps you get an overview of the quality of your applications and to spot trends in your releases.

We notify you

We notify you

We notify you when errors starts happening using Slack, Microsoft Teams, mail or other forms of communication to help you react to errors before your users do.

We help you fix bugs

We help you fix bugs

We help you fix bugs quickly by combining error diagnostic information with innovative quick fixes and answers from Stack Overflow and social media.

See how we can help you monitor your website for crashes Monitor your website