Caching in Nodejs with memory-cache and Redis

 Once the application is developed and out there in the market, the next thing to look at is performance optimization. In thing article, we will cover how to improve our application through caching. We will cover caching using memory-cache and Redis.

A server needs to get lots of data to process the request. for that, we depend on third-party APIs as well as our Database. APIs, involve network calls, which is time-consuming and the Database needs to perform read/write operations, which are time-consuming as well.

What is Caching?

Caching is the process of storing the data in memory so that it can be accessed more quickly than from the original source. The goal of caching is to save the server from processing similar data again and again. Instead, it can store it on a cache and can be retrieved fast.

In Computer Architecture, Cache Memory is a special very high-speed memory. A cache is the fastest memory in the system after Register.

Here cache does a similar job, but it is not the same space we are talking about.

Why Use a Cache?

A cache is used in the case when the data rarely changes. So it saves the server from processing to get the same output. Let's take an example, products in e-commerce, countries and their details. Once a product is saved, there will be more frequent read (client request for products lists) than the addition of new products.

Let's assume, we depend on an external API to get countries and their details, Once we make an API call, those data will rarely change. So the efficient method is the make the API call once and save the response.

Cache helps in reducing the Database IO and API calls and improving the response time, but there is something called "Cache Inconsistency", Cache Inconsistency occurs when the data is Cache is different than data in disk or database.

Working with Cache in Nodejs

In this article, we will look at memory-cache and Redis for caching. memory-cache is In-Memory caching. It's easy to implement. Redis is in Memory Database. so we need to set up a server to start using Redis.

There are other similar packages available for caching, the reason I pick memory-cache is that even though this does not have many updates, the code is clean and fulfils the need.

Caching with memory-cache in Nodejs

Let's First create a simple express app with no caching.


const express = require('express');
const axios = require('axios');

const app = express();

app.get("/", (req, res, next) => {
res.json({
message: "Hello World"
})
})

app.get("/countries", (req, res, next) => {
axios.get('https://countries.bloggernepal.com/countries/').then(response => {
// console.log(response)
res.json({
...response.data
})
})
})


app.listen(5000);
console.log("server started at :5000");

In the above code, we have endpoint /countries, which depends on a third party API to get the list of countries. So every time a client request that endpoint, we make a request to that API, get a response and response back to the client. We have the opportunity to use cache here.

There can be other similar endpoints, where we may what the server to use the cache, so we can implement a middleware, which will respond from the cache if it exists there, else will get the data, cache it and respond.

Caching Entire Response in express middleware

lets create a directory called 'service' and have cache.js there. For now, our cache.js will simply export the memory-cache.


const cache = require('memory-cache');

module.exports = cache;

Let's create a directory called middlewares, and have cacheMiddleware.js there


const cache = require('../service/cache');

module.exports = (req, res, next) => {
let key = `${req.method}:${req.originalUrl}`;
// console.log(key);

let body = cache.get(key);
// console.log(body);

if (body) {
return res.json(body);
} else {
res.jsonFunc = res.json;
res.json = (body) => {
cache.put(key, body);
res.jsonFunc(body);
}
next();
}
}

Now we will use this middleware in our endpoint where we want to use the cache.

In the above code, first, we get the key as `${req.method}:${req.originalUrl}`. so for this request, the key will be 'GET:/countries'. we try to get the response body from the cache, if there is any we will respond it there directly, else

we save the reference of the res.json to res.jsonFunc and replace the res.json with a new method, which will first, put the response body to the cache and then respond to the client. So next time it will be responded from the cache.


const express = require('express');
const axios = require('axios');

const cacheMiddleware = require('./middlewares/cacheMiddleware');

const app = express();

app.get("/", (req, res, next) => {
res.json({
message: "Hello World"
})
})

app.get("/countries", cacheMiddleware, (req, res, next) => {
axios.get('https://countries.bloggernepal.com/countries/').then(response => {
// console.log(response)
res.json({
...response.data
})
})
})


app.listen(5000);
console.log("server started at :5000");

Here we used the cachedMiddleware for '/countries'. So when the res.json method is fired, it will first save that data is cache, Next time, the data will be forwarded from the cache.

Caching is Nodejs, Image from Chrome Network Tab, with response time
Browser Network Tab, Disable Cache (for browser)

As we can see in the above image, The initial response took some 334 ms, and other subsequent requests took 8ms - 18ms. That's lots of improvement. Ya This is localhost, but you get the point here.

Caching Data in for processing

In some cases, we may use the response from an API call or Database to process against some other data, which may be different for each individual. In this case we may want to save the common data on the cache.


//some authenticated route
app.get("/myData", async (req, res, next) => {
// in some case, we have to process data
// where some are user specific while some are common for all users
// let's cache the common data

// for simplicity we will use the same data from the External API call
let countries = cache.get('countries');
if (!countries) {
countries = await axios.get('https://countries.bloggernepal.com/countries/').then(response => {
return response.data
});

cache.put('countries', countries, 1000 * 60);
// if the data is not in the cache, get it and save it
// we can set when wil the cached data to be expired
// it can be choosen based on the data,
// if it changes frequently, lower the time value
// if data dones not changes frequently, we can have some higher vale.
}

// get personalized data from database
// process this data aginst the cached data
// response
let processedData = {};

// stuffs
// stuffs
processedData.countries = countries;

res.json({
message: "your data",
data: processedData
})
})

Remember we talked about that "Cache Inconsistency", one way to handle this is, setting the expiration time for the data to be on the cache. Once the data is expired, the cache is forced to get a new copy of the data.

Here we cache some data, that is not the entire response body, but the data, that is used by all the requesters and is the same. So it prevents the network call every time (Database query). For simplicity, we are using the same API call.

So Why not use memory-cache and look for other solutions?

If you want a simple solution and cannot / (don't want to) install other applications on the server. If you are running your node as a single instance, memory-cache is okay, but you may want to get the benefits for multiple cores with load-balancing among them.

Learn Load Balancing a Node.js Application

In multiple instances of servers, each instance will have its own copy of cache, just occupying more memory than required.

The cached data is not persistent over the server lifespan, will be cleared on restart.

Caching with Redis in Nodejs

We will first install Redis, install redis client to communicate with Redis from Nodejs. But first let's talk about the Redis itself.

What is Redis?

The official.

Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache, and message broker. Redis provides data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, geospatial indexes, and streams. 

Redis is an in-memory database, that saves data in memory, thus have fast access. Redis saves data in key-value pairs, thus it will be easy for us to implement on our system as well.

Let's first download and install it. download guide. Once installed 


$ redis-cli ping
PONG

Let's create a redis.js in service


const redis = require('redis');

const client = redis.createClient();
client.connect();

module.exports = client

We can create similar middleware as above.


const client = require('../service/redis');

module.exports = async (req, res, next) => {
let key = `${req.method}:${req.originalUrl}`;
// console.log(key);

let body = await client.get(key);
console.log(body);

if (body) {
body = JSON.parse(body);
return res.json(body);
} else {
res.jsonFunc = res.json;
res.json = (body) => {
client.set(key, JSON.stringify(body));
res.jsonFunc(body);
}
next();
}
}

Now we are using Redis for caching, you can similarly use Redis to save data as well. set keyword is used to set the key and get to get the key. Redis return Promise so you have to await it.

Cache Hit

A cache hit is said to occur when a request to get a cache is successful when there is data in the cache with that key.

Cache Miss

A Cache miss is said to occur when a request to get a cache failed, as there was no data there, then we need to make a database call or third-party call to set the data.

You can find the source code in this repo

Conclusion

In this article, we learn about Cache. We learn that with cache, we can improve the response time but also reduce the processing need for the server. We used the easy memory-cache as well as the Redis server to set up our cache. Each having own benefits or drawbacks.

Posted by Sagar Devkota

0 Comments