Caching Nodejs applications using node-cache

sujesh thekkepatt
5 min readDec 14, 2019
Photo by Christiann Koepke on Unsplash

Caching is the process of storing data that are accessed frequently and changed less often. This will help in the delivery of data faster, thus improve your application performance.

Node-cache is one of the popular NPM packages for caching your data. Here we are implementing the caching as a part of the application code. ie As an in-application cache. Node-cache is actively developed and supported. You can also be a contributor by going in to the repo participating in the issues. It has 2.9M/month active downloads and 700k weekly download stats in NPM.

Node-cache supports events. Events are emitted for various operations like the set, expire, flush, flushStats, etc. So that we can leverage the advantage of event-driven paradigm.

Let’s create a simple application using express and Postgres and optimize it using node-cache. So that you will get an idea of using node-cache in your application and how caching will impact the application performance. We are creating an API endpoint using express which exposes a route /widgets/:id/. This route will retrieve the data from the widget table corresponding to that id. We will send the data as a JSON response. So the key things that we are going to do are,

  • We will load test the endpoint using autocannon and detect the time of response.
  • We will implement caching using node-cache and load test again.
  • We will analyze the results.

Why load testing? We have to test our endpoints under heavy load to understand how our application performs. The normal load will not differentiate any performance degradation between the cached and uncached versions. You will not find any problems with your endpoint on the normal load. We will be using Sequelize ORM for connecting and fetching data from Postgres. You can find the source code for the application in this git repo.

So let’s start coding the application. We will start by installing Express,Sequelize and necessary postgres package.

npm i — save express sequelize pg pg-hstore

We will start by creating a widget model that stores a config data for a widget that is used by many users. Given below the simple definition of the widget model,


const widget = sequelize.define('widget', {
name: {
type: Sequelize.STRING,
allowNull: false
},
config: {
type: Sequelize.STRING
},
widgetId: {
type: Sequelize.TEXT
}
}, {
timestamps: true
});

Now let’s add some fake data to the database. The data is used as the configuration for the widget. This configuration is sent to the front end and used for rendering widget. So let’s create that endpoint and monitor it. Given below the code of route,

app.get('/widgets/:id', async (req, res) => {let data = await widget.findOne({
where: { widgetId: req.params.id }
})
data = JSON.stringify(data);
res.send(data);
}
});

Here we will be fetching the data from widget the table corresponding to the id. No cache set up done.

Now we will begin to load testing it. I will be using autocannon which is a popular HTTP benchmarking tool. There are several tools out there for benchmarking. You can use anything you like. I will be testing the endpoint for 3 kinds of load,

  • 1k requests
  • 10k requests
  • 100k requests

We can install autocannon using the following command,

npm i -g autocannon

Now open up your terminal and type the following,

autocannon -a 1000 http://localhost:8000/widgets/9nwed_s

Don’t forget to replace the widget id with your widget id. Now verify the result. Given below the results from my machine. There may be a slight difference in the time compared to yours.

Now let’s implement the cache layer using node-cache. So given below the route code with cache implemented,

app.get('/widgets/:id', async (req, res) => {if (nodeCache.get(req.params.id)) {
res.setHeader('n-cache-status', "HIT")
res.send(nodeCache.get(req.params.id));}
else {
let data = await widget.findOne({
where: { widgetId: req.params.id }
})
data = JSON.stringify(data);
nodeCache.set(req.params.id, data);
res.setHeader('n-cache-status', "MISS")
res.send(data);
}
});

Here we check for the id in the cache. Then we will set a cache-status header and send the response. If a cache miss occurs then we will fetch the data from the database, stringify it, set it to the cache and send the response.

Now let’s bench mark the code again and see the results.

From the results, we can analyze that the results are quite impressive. In the uncached version, as the load increases response time started to decrease. Each time we request the data the DB access is performed in the uncached version. Which results in decreased performance. Whereas in the cached version we simply perform a look-up that happens in constant time and results in a drastic improvement in the performance. We must also cautious about the cache overgrowth. Since overgrowth may result in server memory run out. So be cautious about that. You can use the maxKeys option to set the number of keys we want to store in the cache while initializing node-cache . Also, you can use thegetStats method to get the current stats of the cache. You can use the flushAll method to flush all the data from cache. If you want to analyze the stats of the cache you can use flushStats which flushes only the stats. Node-cache also supports take function which can be used to access and delete one time used data like OTP's.

Caching can be offloaded to any dedicated service like Redis or Memcached etc. However, you can still use an application-level cache like node-cache. You can also use cloud flare caching. This will avoid direct traffic to your server. Don’t forget to invalidate the cache when an update occurs for the data. Cloud flare supports cache purging using an API call.

Things to consider while creating a cache,

  1. Cache the data which accessed more often but updated less often.
  2. Always load test your endpoints to ensure it scales well. Implement the cache layer load test it as well. Compare the performance.
  3. Be Cautious about the cache-busting mechanism. Aware when to invalidate or update the cache data.
  4. Don’t allow your application cache to grow beyond a certain limit. If cache growing out of control you have to consider offloading it to external services like Redis etc.
  5. Avoid using maxKeys:-1 option. It will allow unlimited keys. Always use a certain limit. Also set useClones:false property. True value always gives a copy of the cache data. This will cause performance degradation when used with large objects.
  6. Remove less used key values from your cache. Use an internal housekeeping method for that.

Hey, I am Sujesh, a developer working on Nodejs. I occasionally write on Nodejs which I find useful. If you like my work and want to support it, buy me a cup of coffee!Thanks.

--

--