Introduction

How do we run scheduled jobs from ASP.NET without requiring a Windows Service to be installed on the server? Very often, we need to run some maintenance tasks or scheduled tasks like sending reminder emails to users from our websites. This can only be achieved using a Windows Service. ASP.NET being stateless provides no support to run code continuously or to run code at a scheduled time. As a result, we have to make our own Windows Services to run scheduled jobs or cron jobs. But in a shared hosted environment, we do not always have the luxury to deploy our own Windows Service to our hosting provider's web server. We either have to buy a dedicated server which is very costly, or sacrifice such features in our web solution. However, running a scheduled task is a very handy feature, especially for sending reminder emails to users, maintenance reports to administrators, or run cleanup operations, etc. I will show you a tricky way to run scheduled jobs using pure ASP.NET without requiring any Windows Service. This solution runs on any hosting service providing just ASP.NET hosting. As a result, you can have the scheduled job feature in your ASP.NET web projects without buying dedicated servers.

How it works

First, we need something in ASP.NET that is continuously running and gives us a callback. The IIS web server is continuously running. So, we somehow need to get a frequent callback from it so that we can lookup a job queue and see if there's something that needs to be executed. Now, there are several ways a web server comes to us:

When a page hits

When an application starts

When an application stops

When a session starts and ends/timeouts

When a cache item expires

The page hit is random. If nobody visits your website for hours, you can't do the pending jobs for hours. Besides, the execution of a request is very short and needs to finish as soon as possible. If you plan to execute scheduled jobs on page execution, then the page will take longer to execute, which will result in a poor user experience. So, clearly this is not an option.

When an application starts, we get a callback in the Application_Start method of Global.asax. So, this is a good place to start a background thread which runs forever and executes scheduled jobs. However, the thread can be killed anytime the web server decides to take a nap due to zero load.

When an application stops, we get a callback at Application_End. But we can't do anything here because the whole application is going to die soon.

Session_Start in Global.asax is triggered when a user visits a page that requires a new session to be initiated. So, this is also random. We need something that fires consistently and periodically.

A cache item expires on a given time or duration. In ASP.NET, you can add entries in the Cache and set an absolute expiry date time, or you can set a duration after which the item is removed from the cache. You can do this by utilizing the following method of the Cache class:

The onRemoveCallback is a delegate to a method which is called whenever a cache item expires. In that method, we can do anything we like. So, this is a good candidate for running code periodically, consistently without requiring any page visit.

This means, we can simulate a Windows Service utilizing Cache timeout! Now, who thought that this would be possible?

Creating cache item callbacks

First, on Application_Start, we need to register a cache item that will expire in two minutes. Please note, the minimum duration you can set for expiring a callback is two minutes. Although you can set a lower value, it does not work. Most probably, the ASP.NET worker process looks at the cache items once every two minutes.

This cache entry is a dummy entry. We do not store any valuable information here because whatever we store here might be gone on application restart. Besides, all we need is the frequent callback from this item.

Store item in cache again upon expire

Whenever the cache item expires, we get a callback and the item is gone from the cache. So, we no longer get any callback in future. In order to have a continuous supply of callback, we need to store an item in cache again upon expiration. This seems quite easy; we can call the RegisterCacheEntry function shown above from the callback function, can't we? It does not work. When the callback method fires, there is no HttpContext available. The HttpContext object is only available when a request is being processed. As the callback is fired from the web server behind the scene, there is no request being processed and thus no HttpContext is available. As a result, you cannot get access to the Cache object from the callback function.

The solution is, we need to simulate a request. We can make a dummy call to a dummy webpage by utilizing the WebClient class in the .NET Framework. When the dummy page is being executed, we can get hold of the HttpContext and then register the callback item again.

Whenever the dummy page executes, the Application_BeginRequest method gets called. There, we can check whether this is a dummy page request or not.

protectedvoid Application_BeginRequest(Object sender, EventArgs e)
{
// If the dummy page is hit, then it means we want to add another item
// in cache
if( HttpContext.Current.Request.Url.ToString() == DummyPageUrl )
{
// Add the item in cache and when succesful, do the work.
RegisterCacheEntry();
}
}

We only intercept the request to the dummy page, and we let other pages execute as it is.

Restart cache item callback on web process restart

There are several cases where the web server might be restarted. For example, if the Administrator restarts IIS, or restarts the computer, or the web process gets recycled (Windows 2003). In such cases, the service will stop running unless a page is hit and the Application_Start is called. Application_Start is called only when a page is visited for the first time in a web project. So, when the web process is restarted, we need to manually call the dummy page, or someone needs to visit the home page of your website, in order to get the service running again.

One tricky solution to this problem can be adding your website to search engines. Search engines crawl pages frequently. So, they will hit a page of your website, resulting in Application_Start, and thus the service will get up and running.

Another idea is to register your website to some traffic or availability monitoring services. There are lots of Web Services which keep an eye on your website and checks whether it is alive and the performance is satisfactory. All these services hit your web site pages and then collect the statistics. So, by registering in such services, you can guarantee your web application is alive all the time.

Testing possible types of job executions

Let's test whether we can do all possible tasks that we can do from a Windows Service. The first problem is, we cannot do everything a Windows Service does because it runs in the Local System account. This is a very high privilege account, and you can do anything to your system under this account. However, the ASP.NET web process runs in the ASPNET account (Windows XP) or NETWORK SERVICE account (Windows 2003). This is a low privilege account, and does not have the right to access the hard drive. In order to allow the service to write to the hard drive, the web process account needs to be allowed write permission to a folder exclusively. We all know about this security problem, so I won't go into details about it.

Now, we will be testing all possible things that we normally do from a Windows Service:

This will create entries in the log table, which you can check later on to ensure if there was any delay between the service execution. You should be getting one row per two minutes.

Figure 1: ASP NET Service Log Table Data

Testing email delivery

The most common requirement for running a Windows Service is to send periodical email alerts, notifications, status reports, etc., via mail. So, it's important that we test if email works from such a service:

Alternatively, you can call the Receive method of the queue to get pending messages in the queue that needs to be processed.

One issue that you should remember here is, do not subscribe to the Receive event of the queue. As the thread can die anytime and the Web Server can be restarted anytime, a continuous blocked Receive will not work properly. Besides, if you call the BeginReceive method and block code execution until a message arrives, the service gets stuck and other codes will not run. So, in this case, you will have to call the Receive method to fetch the pending messages.

Extending the system

The ASP.NET Service can be extended in many ways to allow pluggable jobs. You can introduce Job queuing from web pages which this service will execute periodically. For example, you can queue jobs in a cache item and the services pick up the job and executes it. This way, you can simulate a nice job processing system inside your ASP.NET project. Previously, this was only possible with a Windows Service, but now you can do it with ASP.NET also.

Let's make a simple Job class which contains information about a job to execute.

So, the job gets queued to be executed after 4 minutes. The service code runs every two minutes and checks the job queue if there's anything that is overdue and needs to be executed. If there's any job waiting, it is removed from the queue and executed. The service code now has an additional method called ExecuteQueuedJobs. This function does the scheduled job execution:

Don't forget to lock the static job collection because ASP.NET is multithreaded, and pages being executed on different threads have a high possibility for trying to write on the job queue at the same time.

Who can benefit from this

ASP.NET developers who host their website on shared hosting environments which do not offer a Windows Service feature can benefit from this code. Also for those who do not have their own dedicated server to install Windows Services.

How to run the sample project

The sample project is a very simple website with a dummy page. All the code is inside Global.asax. You need to modify the connection string, MSMQ name, and the folder path according to your settings.

Here's how to setup the code:

Extract the zip in a folder.

Create a virtual directory named "TestCacheTimeout" and map to the extracted folder.

Load the project file and run.

Create an MSMQ Queue called "ASPNETService".

Run the SQL to create the log table in tempdb, as explained earlier.

Create a temp folder in C: drive and add the ASPNET/NETWORK SERVICE account with Write permission.

Fix all the constants available in Global.asax.

If you want to add the code in your own project, do the following:

Copy the Global.asax code to yours.

Create a dummy page which queues the cache item. The dummy page needs no code.

Thanks, first look your solution is very short and effective. However, if the website is idle for 10-15 minutes, the Application_End is fired and the timer is shutdown. This logic is no longer active until a user visit the site to initiate the Application_Start again. How do we go around that issue in your solution?

The scheduler is based on the Application object of the web server.
In this project the Application object is kept alive by calling a dummy page on the web server etc. To my opinion this can be done in a more simple and controllable way.

another approach
By controlling the Application Pool properties, the timeout of the Application object can be set to infinity, so when started, the web Application is run forever.

Why not instantiate a static worker class on this Application object? This can easily be done in Global.asax

I have build a few of these web server scheduled services and they work like a charm. Special attention to exception handling in the worker class. If it goes down, the web server has a real tough time also.

Thanks, similar to the post above, your solution is short and effective. However, if the website is idle for 10-15 minutes, the Application_End is fired. This logic is no longer active until a user visit the site to initiate the Application_Start again. How do we go around that issue in your solution? [edit] sorry, just noticed you also mentioned setting application pool timeout to infinity. (Ref: MSDN Performance Application Pool Settings[^]) Could you explain how to do that in code exactly? Would that cost any side effects assuming the site is running on a shared web hosting server?

I try Using HttpRuntime.Cache instead of HttpContext.Current.Cache. However it still didn't help the web application to go to idle mode, and the service will stop running until someone hit the web site again.

Are u sure that you will able to keep your service/job processing running even no one hit the web site for a few hours?

kudos to the author for putting a well constructed article. It made me understand a lot of things the IIS does in the backend to process a request.

I too had the same problem in my shared hosting. Instead of going through all the pain this article describes I installed a webservice in my site and then created a java me app to call this webservice at regular intervals. Once the webservice is called it will trigger my code which is supposed to act as a scheduler.

I made the Java me(J2me) app run in the background in my phone(like the alarm clock in your phone) so even when the phone is locked it keeps on triggering the webservice.

And to have a good look at the task that was completed I email or sms myself the result of the trigger.

'Progress isn't made by early risers. It's made by lazy men trying to find easier ways to do something.' Robert Heinlein (1907 - 1988)

In my Global.asax file, i am getting the error
Error 'System.Web.Caching.CacheItemRemovedCallback' is a delegate type and requires a single 'addressof' expression as the only argument to the constructor.