The Coding Dojo

Hush quiet web site, go to sleep.

For most of us, most of the time, we are able to prevent our web apps from unloading when they become idle, but what can you do if you don't have access to your server's config? Now, just to be clear, what I mean by "don't have access to the server's config" is, you aren't able to set your server config to keep your site alive, or prevent your app from unloading when idle.

Normally you may adjust your IdleTimeout when dealing with IIS, or choose the Always On option in Azure app services. But what happens when you are using a web hosting service that doesn't give you access to these options?

Who cares, let it rest.

Does it really matter if your site goes to sleep? That depends entirely on what your site is doing. Your site going to sleep can disrupt any of the following:

  • Background and scheduled tasks.
  • Your site acts as some kind of hub e.g. SignalR.
  • Initial response times.
  • Cached data and processes.

Even if your site isn't doing anything particularly clever, the initial response time after your app goes to sleep may take as long as 7 or 8 seconds, maybe more depending on your provider and level of service. This isn't good, especially with today's fast-paced, "gimme-it-now" attitude, which I suffer from terribly.

The solution.

Fortunately, the solution is wonderfully simple, so simple in fact, even if you don't care about your site sleeping, you may as well go ahead and do this anyway. Ultimately, if your site is active, it stays "alive". Well then, let's keep it active.

.Net core provides us with an awesome background processing framework in the form of IHostedService. "But wait" I hear you yelp, "You said background services are affected by this whole going to sleep thing?". You're right, I did, but what if the background service is also responsible for keeping your site alive, like a heartbeat ❤ calling your site every now and again with simple request, keeping it awake and responsive. Are you with me? This is definitely not a new concept or idea. I just wanted to show you how you may want to do it using IHostedService.

Add your heartbeat endpoint.

Create yourself a lightweight endpoint that your heartbeat can hit. This endpoint can be a simple GET, with no special logic and shouldn't require any security. I'm going to add mine to my HomeController and call the action Alive. This guy is simply going to return an Ok result.

public class HomeController : Controller 
{
    public IActionResult Alive () 
    {
        return Ok ();
    }
}

Create your hosted service

Next you need to create a class which implements the IHostedService interface which you can read more about here. This interface provides the StartAsync method which is called automatically via the DI system when your app starts, and the StopAsync method which is called automatically via the DI when your app stops.

 public class KeepAliveHostedService : IHostedService, IDisposable
    {
        private Task _task;
        private CancellationTokenSource _cancellationTokenSource;
        private readonly HttpClient _httpClient;

        public KeepAliveHostedService()
        {
            _httpClient = new HttpClient();
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            // This is called automatically when the app starts.

            _task = RunAsync(cancellationToken);

            return _task.IsCompleted ? _task : Task.CompletedTask;
        }


        private async Task RunAsync(CancellationToken cancellationToken)
        {
            _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

            // I want to call my endpoint at random intervals, between 10 and 60 seconds
            var updateInterval = 10000;
            var random = new Random(DateTime.Now.Millisecond);

            // Keep on loopin'
            while (!cancellationToken.IsCancellationRequested)
            {
                try
                {
                    await _httpClient.GetAsync("https://yoursite.com/Home/Alive", cancellationToken);
                    updateInterval = random.Next(10000, 60000);
                }
                catch (Exception ex)
                {
                    // Logout, or use AppInsights etc
                }

                await Task.Delay(updateInterval, cancellationToken);
            }
        }

        public async Task StopAsync(CancellationToken cancellationToken)
        {
            // This is called automatically when your app stops.

            if (_task == null)
                return;

            _cancellationTokenSource?.Cancel();

            await Task.WhenAny(_task, Task.Delay(-1, cancellationToken));

            cancellationToken.ThrowIfCancellationRequested();
        }


        public void Dispose()
        {
            // Your usual dispose/clean up stuff
            _cancellationTokenSource?.Cancel();
        }
    }

As you can see, in each loop, we are making a request to our alive endpoint. It's this heartbeat that will keep your site active when there is no actual user activity.

I decided to make my heartbeat intervals slightly random, between 10 and 60 seconds. If you prefer, you can just choose a constant frequency, let's say once every 30 seconds, though it may be worth checking with your hosting provider, on how long does your app need to be idle before going to sleep, then make sure your heartbeat frequency is less than that time.

Registering your new service.

The last thing to do is register your hosted service, adding it to the DI collection. You do this as you would any other service, from within your startup class, and it's ConfigureServices method. Add a singleton of type IHostedService and specify your new implementation KeepAliveHostedService. IHostedService services will be instantiated automatically via the DI system when your app starts up.

public class Startup 
{
    public Startup (IConfiguration configuration) 
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }
    
    public void ConfigureServices (IServiceCollection services) 
    {

        // other configureService setup stuff

        services.AddSingleton<IHostedService, KeepAliveHostedService>();
    }
    ...
}

And that's it, we're done! Told you it was simple. Your site will now remain alive and responsive. Any other background processes will continue to run without any disruption, and cached items will remain in memory until you decide to remove them.

Fin!