fermentationist

JavaScript Developer. I love to build things, in code and otherwise. I also brew a righteous Kölsch.

Prevent Your Free Heroku Dyno from Sleeping

If you are like me, then many of your hobby projects and portfolio examples are deployed to Heroku. Like Github Pages, it is an attractive option to host your project because it is free. Unlike Github Pages, you can host a server on Heroku, so I use it whenever I need a free place to deploy a full-stack application.
There is, of course, a catch, which is that your free Heroku dyno (virtual machine instance) will take a nap whenever it has gone for thirty minutes or more without receiving any web traffic.
A user who tries to load an application with a sleeping dyno will experience a wait time of up to ten seconds, which is a sufficient amount of time for many users to conclude that your app is broken and move on. Especially in instances when you are using Heroku to showcase your work, like a portfolio project, this is a real problem.

An easy solution for Nodejs

Fortunately, it is really easy to keep your dyno awake with just a few lines of code. I used Nodejs, because that is the language my back end is written in. For the sake of reuse, I created a small module, though a simple function is all that is required.
I used Node's
setTimeout
function to make an HTTP request to my app at regular intervals of less than thirty minutes, thus keeping the dyno from snoozing. Why not use
setInterval
instead? It automatically repeats and would be slightly more concise, after all. You certainly could, but I have seen some accounts that
setInterval
can be problematic. Though it is admittedly unlikely my use case would run afoul of this type of problem, it also turned out that error handling was easier using setTimeout, as you will see.
I named my little utility function
wakeUpDyno
, and used a
modules.export
statement to declare it as the module's default export. I only needed to install one dependency:
node-fetch
, though any Node library that makes HTTP requests will work.
The function has two parameters,
url
- the address of the dyno (a string) – and
interval
– the amount of time in minutes that our function will wait between making its HTTP requests (an integer).
setTimeout
is called from inside
wakeUpDyno
, passing it both an anonymous callback function, and the length of time it is to wait in milliseconds between calls (the time in minutes – interval – multiplied by 60,000 ms/min).
Then, to wake the dyno, I put the HTTP request inside the callback function, and invoked it with the
url
argument.
const fetch = require("node-fetch");

const wakeUpDyno = (url, interval) => {
    const milliseconds = interval * 60000;
    setTimeout(() => {
        fetch(url);
    }, milliseconds);
};

module.exports = wakeUpDyno;
This will do the trick of rousing a sleepy dyno, but unlike
setInterval
,
setTimeout
will only run once. So, I needed to make the function call itself again after finishing its
fetch
request.
It occurred to me that if a fetch call were to fail, then function execution would never make it to my recursive call, and the program would exit with an error. So I decided to nest the
fetch
request inside a
try...catch
block, followed by a
...finally
block containing the recursive call. This way, the function will call itself again, whether or not the previous
fetch
call fails.
const HTTP = require("HTTP");

const wakeUpDyno = (url, interval) => {
    setTimeout(() => { 

        try { 
            HTTP.get(url, () => {
                console.log(`Making HTTP request to ${url}...`)
            });
        }
        catch (err) {
            console.log(`Error fetching ${url}`);
        }
        finally {
            wakeUpDyno(url, interval);
        }

    }, interval);
};

module.exports = wakeUpDyno;
At that point, my utility was functional, but I also wanted to add an optional
callback
parameter, and also to give the
interval
parameter a default value of 25 minutes.
In case of its failure, I nested the invocation of
callback
in another
try...catch...finally
block, situated in the original
...finally
block. I moved the recursive call to
wakeDyno
down to the more deeply nested second
...finally
block, so that it would execute regardless of the success of
callback
.
So, my final code looks like this:
const fetch = require("node-fetch");

const wakeUpDyno = (url, interval = 25, callback) => {
    const milliseconds = interval * 60000;
    setTimeout(() => {

        try { 
            console.log(`setTimeout called.`);
            // HTTP GET request to the dyno's url
            fetch(url).then(() => console.log(`Fetching ${url}.`)); 
        }
        catch (err) { // catch fetch errors
            console.log(`Error fetching ${url}: ${err.message} 
            Will try again in ${interval} minutes...`);
        }
        finally {

            try {
                callback(); // execute callback, if passed
            }
            catch (e) { // catch callback error
                callback ? console.log("Callback failed: ", e.message) : null;
            }
            finally {
                // do it all again
                return wakeUpDyno(url, interval, callback);
            }
            
        }

    }, milliseconds);
};

module.exports = wakeUpDyno;
To use it, I will
require
it in my project, and invoke it when I start up my Express server, like this:
const express = require("express"); 
const wakeUpDyno = require("wokeDyno.js"); // my module!


const PORT = 3000; // whatever port you like
const DYNO_URL = "https://howimadeathing.herokuapp.com"; // the url of your dyno

const app = express(); // instantiate Express app

app.listen(PORT, () => {
    wakeUpDyno(DYNO_URL); // will start once server starts
})
And that is it! Once I uploaded it to Heroku, it worked exactly as intended, and my app hasn't slept since!
One small caveat to this project: Heroku allows users a limited number of free dyno hours per month, so if you have multiple apps running this utility, you may run out! For that reason, in a future iteration of this module, I intend to allow scheduling so dyno hours may be rationed more effectively.
Please reach out to me with any feedback or corrections. You can access the Github repository for this project, here.

Tags

Topics of interest