How To Boost Your Node.js Performance by Implementing Caching With Redis.
Tags: node, caching, redis, webdev
Published: 2024-02-27 00:09:27

### Introduction 
As your application grows and attracts more traffic, it becomes increasingly important to ensure faster response times. This helps prevent users from experiencing delays and frustration while using your app, as slower response times can make you lose users and revenue. By implementing caching, you can optimize and scale your application, providing users with faster response times and a great user experience.

Storing frequently accessed data in a location (cache) that is quicker to access is an effective way to improve the performance of your Node.js application. Caching reduces the time and resources required to query the main memory (database), which in turn reduces server load and speeds up content delivery to users.

In this guide, you will learn the fundamentals of Caching and Redis. You will also learn how to implement caching in a Node.js application using Redis. This will be done with a simple blog example built with Node.js, MongoDB, Express, and Express Handlebars. 


## Prerequisites

To follow this guide, ensure you have the following: 

1. A basic understanding of [Node.js](https://nodejs.org/en/download/), [Express](https://expressjs.com/), and [Express handlebars](https://ejs.co/).
2. An [Upstash](https://console.upstash.com/login) account.
3. A MongoDB Database ([Atlas](https://www.mongodb.com/atlas)). If you're unsure how to retrieve your MongoDB Atlas connection string, check out the tutorial — [A step-by-step guide: How to use MongoDB with Node.js to set up MongoDB Atlas](https://dev.to/backendbro/using-mongodb-with-nodejs-175l) for guidance.

**TL;DR**: you can find the entire code used in this guide on [GitHub](https://github.com/backendbro/caching-in-action-complete).

## An Introduction to Caching.
**What is a Caching?**
Caching is the process of storing data in a cache.  When we query for data from a source (main memory or a database), we have to wait for some time to get a response. But if data is stored in a fast access memory like [Redis](https://redis.io/), data retrieval time can be cut in half, and we get to prevent a bottleneck caused by a large number of users trying to access the same piece of data.

**Use case**
Have you ever noticed that when you visit your favorite website, it always seems to know what you're interested in? Social media platforms like [Facebook](https://web.facebook.com/?_rdc=1&_rdr) also use your cached information to make friend suggestions and show you posts that are relevant to your interests. By using your cached data, these websites aim to improve your user experience and make your online activities more enjoyable.

**Cache internals and mechanism**
Caches are designed to be small, fast, and lightweight. However, this limits their capacity to hold large amounts of data for extended periods. To ensure that they always have up-to-date information, caches work in tandem with databases. Because caches are small, it is important to regularly remove old or outdated data and replace it with fresher information. One technique used to accomplish this is **Time-To-Live** (TTL), which moves expired data out of the cache to make room for new data. Caching algorithms are also used to evaluate the data in the cache and determine which data has the lowest priority and should be removed. 

**Setting up a Cache network**
When setting up a cache network, it's essential to consider the data that users frequently access. This is because you don't want to cache unnecessary information that will occupy valuable space. To achieve a high cache hit rate, a cache policy that works best for your application must be carefully implemented. For instance, the "**write-through cache**" policy ensures that the cache and database are updated simultaneously. 



![write-through cache illustration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r0q2974rgz7q29k7mvq6.png)


However, there is a cost associated with this policy as it may not be suitable for write-intensive applications. This is because your application will have to store data in two locations, leading to performance issues. 

On the other hand, if your application is write-intensive, the "**write-back cache**" may be more suitable, as it only writes data to the cache at first and later updates the database. This way, data is not stored simultaneously in two locations, preventing performance issues.



![write-back cache](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j0yq9v3bp9sicvz1mih0.png)


It's crucial to consider the type of application you're building before selecting a caching policy and algorithm to prevent further problems in your application.


## An Introduction to Redis
**What is Redis**
[Redis](https://redis.io/) is an open-source, in-memory data store that can be used as a database, cache, and message broker. Redis is well-known for its high performance, flexibility, and scalability, making it a popular choice for modern web applications.


Redis's in-memory data model allows it to store and access data directly from the server's RAM, rather than being written to disk like traditional databases. As a result, Redis offers faster data access and retrieval times and lower latency since disk I/O operation is reduced. 

**Redis data storage**
In addition, Redis stores data using a key-value pair, where each key is a string, and each value can be any of the supported data structures. 


![What is Key Value Pair? How can you automate key-value pair extraction?](https://assets-global.website-files.com/5f689f82910c6b4f1ffb855b/6138a0d331b6f841ee6de3b0_What%20is%20Key%20Value%20Pair%20Extraction-min.jpg)


Redis is capable of supporting a variety of data structures, including strings, hashes, lists, sets, and sorted sets, making it well-suited for real-time analytics, caching, and message passing.

**Use case**
Redis is a powerful tool for caching data. Its high throughput and availability make it an excellent choice for implementing caching, as it ensures a high cache hit rate. This means that frequently accessed data are served up faster, reducing the time it takes to retrieve data from the main memory (database). Redis is also used for several use cases such as chat, messaging, queues, gaming leaderboards, session stores, and much more.

Overall, Redis is a powerful and versatile tool that can help developers build faster, more scalable, and more reliable applications. 


## Setting up Redis.

In this guide, we will use a Redis server service named [Upstash](https://console.upstash.com/login). Although other web services such as [AWS Lambda](https://aws.amazon.com/lambda/), [Azure Functions](https://azure.microsoft.com/en-us/products/functions/), and [Google Cloud Functions](https://cloud.google.com/functions/) offer Redis cache, we will use Upstash to navigate this tutorial. 


1. To get started, visit [Upstash](https://console.upstash.com/login) to create an account. If you already have one, you can log in.


2. After setting up your account, click the "**Create database**" button to create a Redis database.
![Upstash-create-database-image](https://paper-attachments.dropboxusercontent.com/s_BE21C680105540F351BAC7689089928A0CFFE2B8C3A67597CAA282BA67110860_1708035271092_annotely_image+8.png)

3. Fill out the form in the pop-up and click "**Create**" to proceed. Refer to the screenshot below for help:
![Upstash-pop-up](https://paper-attachments.dropboxusercontent.com/s_BE21C680105540F351BAC7689089928A0CFFE2B8C3A67597CAA282BA67110860_1708635443006_annotely_image+29.png)



4. Viola! we have successfully set up a Redis cache. 
![Upstash-dashboard](https://paper-attachments.dropboxusercontent.com/s_BE21C680105540F351BAC7689089928A0CFFE2B8C3A67597CAA282BA67110860_1708036302714_annotely_image+10.png)

5. To retrieve your connection string; scroll down, click on the “**Node**” tab, and select "**ioredis**". Refer to the screenshot below for help.
![Node-tab](https://paper-attachments.dropboxusercontent.com/s_BE21C680105540F351BAC7689089928A0CFFE2B8C3A67597CAA282BA67110860_1708635579966_annotely_image+30.png)

    > _Hover over the password tab to copy your password. Then, replace the star symbols in your connection string with your actual password._

6. Make sure to keep your Redis connection string safe and ready, as it will be needed in the next section when we implement application caching.


## Caching in Action

To illustrate how caching improves the performance of a Node.js application, we will use a basic blog as an example. This blog has three main functionalities: creating a post, listing all posts, and viewing a post. Let's begin! 

### Project setup
1. To proceed, go to [GitHub](https://github.com/) and clone this [boilerplate](https://github.com/backendbro/caching-in-action-boilerplate).

2. After cloning the boilerplate, run the following commands in your terminal one after the other to install all the dependencies required:     
    ```javascript

    npm i 
    npm i ioredis   

    ```

    #### Quick note:
    > **[ioredis](https://github.com/backendbro/caching-in-action-boilerplate)** _is a popular Node.js client library with a simple and high-performance interface that helps developers to easily interact with Redis servers_.
 
3. Create a **`.env`** file in the root directory.

    ![Process.env](https://lh7-us.googleusercontent.com/tEPWI6C_OKohh2irH6rIrzkDg9c95rsrfHwqQn4PgaLlKuhO-hSoCd4XVkbTpGCov3Tl9RKlvPS5WGogycmbIc3o2AF_has8lqITYUOImcbX-m1lV0ZCFPHCOzCooccfaaYLo0EM71A3wtPFKUqPeyg)

    Next, enter the following environment variables into the newly created file. 
    ```javascript
    PORT = 8000 
    mongodb_uri = yourMongoDBConnectionString
    redis_url = yourRedisConnectionString
    ```



    .
4. To ensure that the application runs smoothly, execute the following command: 

    ```javascript
    npm run dev
    ```

    ![npm-run-dev](https://paper-attachments.dropboxusercontent.com/s_BE21C680105540F351BAC7689089928A0CFFE2B8C3A67597CAA282BA67110860_1708548351516_annotely_image+18.png)

5. To access the application, open your web browser and type in the following URL — **`localhost:8000/dashboard`**. This will take you directly to the dashboard.
    ![boilerplate](https://paper-attachments.dropboxusercontent.com/s_BE21C680105540F351BAC7689089928A0CFFE2B8C3A67597CAA282BA67110860_1708637069961_annotely_image+31.png)

### Writing code
In the section: "**Introduction to Caching**", we discussed caching policies and how selecting the right one can boost an application's performance. In this example blog, we will implement the "**write-through cache**" policy. This policy involves storing a post in both the cache and the database simultaneously. Since blogs are generally read-intensive, this policy will be the most appropriate. 


1. Create a folder named **`redis-store`** in the root directory. Inside the **`redis-store`** folder, create a file named **`redis.js`** for the redis connection.
![redis-store](https://paper-attachments.dropboxusercontent.com/s_9996436E73771A7162D97E7EAE012E4EF28A7A3DE0F281F57BD53DC22A8B0341_1708457411863_Screenshot+2024-02-21+012952.png)

2. Open the `redis.js` file and add the following code to connect to Redis.
    ```javascript
        const Redis = require("ioredis")
        const dotenv = require('dotenv')
    
        dotenv.config()
    
        const redisClient = () => {
        const redis = new Redis(process.env.redis_url)
    
         redis.on("error", (err) => {
            console.log(`Redis crashed: ${err.message}`)
         })
     
         redis.on("connect", () => {
            console.log(`Redis started on ${redis.options.host}`)
         })
     
         return redis
         }
    
         module.exports = redisClient()
    ```

    .

3. Navigate to the **`controller/Post.js`** file, and make the following changes to incorporate caching into the code.  

    **a.** First, import the Redis connection. 
    
    ```javascript
    const Redis = require('../redis-store/redis')
    ```
    
    **b.** To cache newly created post, update **`addPost`** function with provided code snippet:
    ```javascript
    const addPost = async (req,res) => {
    try {
        try {
          const newPost = new Post({
            title: req.body.title,
            body: req.body.body
          });
    
          
          const post = await Post.create(newPost);
  
          // Caching newly created post
          await Redis.set(post.id, JSON.stringify(post), 'EX', 
          604800)
          await Redis.del("allPosts")

          res.redirect('/dashboard');
        } catch (error) {
          console.log(error);
        }
    
      } catch (error) {
        console.log(error);
      }
    }
    ```    
    
    In the code snippet above: 
      - We cached each post as soon as it was created.
      - An expiration time of either 604800 seconds or 7 days was set to expire the post from the cache.
      - We also deleted the "**allPosts**" entry from the cache because it would be outdated once a new post is added to the database. 
      - Finally, we are redirected to the dashboard to view the newly created post.

    **c.** Modify the **`viewPost`** function to enable the display of specific posts from either the database or cache: 
    ```javascript
    const viewPost = async (req,res) => {
      try {
          let slug = req.params.id;
          let data;
    
          const redisData = await Redis.get(slug)
          data = JSON.parse(redisData)
         
          if (data === null){
            data = await Post.findById({ _id: slug });
            await Redis.set(slug, JSON.stringify(data), 'EX', 604800)
          }
      
          const locals = {
            title: data.title,
            description: "Simple Blog created with NodeJs, Express & MongoDb.",
          }
      
          res.render('post', { 
            locals,
            data,
            currentRoute: `/post/${slug}`
          });
        } catch (error) {
          console.log(error);
        }
    }
    ```
    
    In the code snippet provided:
      - We first check the Redis cache to see if a post matching the provided ID exists.
      - If it does, we return it. 
      - However, if there is no match in the cache, we check the database for the data, cache it, and then return the post to the user.


    **d.**  Lastly, update the **`getPosts`** function to retrieve all posts from the database or cache: 
    ```javascript
    const getPosts = async (req,res) => {
        try {
            const locals = {
              title: 'Dashboard',
              description: 'Simple Blog created with NodeJs, Express & MongoDb.'
            }
    
            const redisData = await Redis.get("allPosts")
            let data = JSON.parse(redisData)
             
            if (data !== null) {
                res.render('Post-crud/dashboard', {
                locals,
                data,
                layout: mainLayout
              });
            }else {
             
              data = await Post.find();
              await Redis.set("allPosts", JSON.stringify(data))
             
              res.render('Post-crud/dashboard', {
                locals,
                data,
                layout: mainLayout
              });
             
            }
          } catch (error) {
            console.log(error);
          }
    }
    ```
    
    In the given code snippet above. 
    - We start by checking the Redis cache to see if there is an entry called “**allPosts**”.
    - If such an entry exists, we directly return “**allPosts**” from the cache.
    - If there is no cached data found, we retrieve “**allPosts**” from the database, and then proceed to cache it.
    - Finally, we display all the posts to the user.



## Testing

Let's test the cache implementation to ensure that it works as expected. 


1. To run your application, open your terminal and type the command 

    ```javascript
    npm run dev
    ```
    .

2. Open your browser and access **`localhost:8000/dashboard`** to begin testing.


3. To create a post, click on the "**Create Post**" button.
![Create-post](https://paper-attachments.dropboxusercontent.com/s_9996436E73771A7162D97E7EAE012E4EF28A7A3DE0F281F57BD53DC22A8B0341_1708461907344_annotely_image+14.png)

4. Provide a title and content for your post, then click “**Add**”.
![Title](https://paper-attachments.dropboxusercontent.com/s_9996436E73771A7162D97E7EAE012E4EF28A7A3DE0F281F57BD53DC22A8B0341_1708462202306_annotely_image+15.png)

5. Upon reaching the dashboard, click on your newly created post to view it.
![New-post](https://lh7-us.googleusercontent.com/1coh6ao5cufkyAM5sMU6GVhNRa1OpZOqc_2CJfg1jE3HddcnhoiFqEXZnTSMG1yb6MsJ_wofqDA5utxE0UFNc2YcnXi7b7M4rTrMrQ9E8lRuXUpgeRPX0iu_QLC6xf5DOOnTpSU60D146Ej59c8dq6g)

6. Yay! The application is working as intended, and we have successfully created a new post. 
![New-stuff](https://paper-attachments.dropboxusercontent.com/s_BE21C680105540F351BAC7689089928A0CFFE2B8C3A67597CAA282BA67110860_1708550047109_annotely_image+19.png)

7. To complete the testing phase, log in to your [Upstash](https://console.upstash.com/login) account to check if both the newly created post and “**allPosts**” are added to the cache.


8. After logging into your Upstash account, go to your dashboard and click on the "**Data Browser**" tab to access the newly added data.
![New-stuff](https://paper-attachments.dropboxusercontent.com/s_BE21C680105540F351BAC7689089928A0CFFE2B8C3A67597CAA282BA67110860_1708551275588_annotely_image+20.png)



9. Click on each entry to confirm the contents of the newly added data.
    - Single post: 
![Single-post](https://paper-attachments.dropboxusercontent.com/s_BE21C680105540F351BAC7689089928A0CFFE2B8C3A67597CAA282BA67110860_1708551613314_annotely_image+21.png)
    - All posts:
![All-post](https://paper-attachments.dropboxusercontent.com/s_BE21C680105540F351BAC7689089928A0CFFE2B8C3A67597CAA282BA67110860_1708551633287_annotely_image+22.png)




## Benchmarking

The moment of truth is here. Let's see if caching improves the performance of an application. We will use the browser dev tools to compare the data retrieval time from the database vs the cache.


### Without Caching: 
In this section, we will comment out all the code blocks where caching was implemented. Then, we will proceed to create a post, retrieve all posts, and view a single post. 

1. **All posts**: It took 635 milliseconds to retrieve all the posts from the database and return them to us.
![Allpost-benchmark](https://paper-attachments.dropboxusercontent.com/s_BE21C680105540F351BAC7689089928A0CFFE2B8C3A67597CAA282BA67110860_1708615973486_annotely_image+27.png)

    
2. **Single post**: It took the database 267 milliseconds to return a single post. 
     
![Single-post-benchmark](https://paper-attachments.dropboxusercontent.com/s_BE21C680105540F351BAC7689089928A0CFFE2B8C3A67597CAA282BA67110860_1708616186162_annotely_image+28.png)

     
### With caching: 
Let's uncomment the caching logic to compare data retrieval time. 

> Make sure to delete your cached data


![How-to-delete-cached-data](https://paper-attachments.dropboxusercontent.com/s_BE21C680105540F351BAC7689089928A0CFFE2B8C3A67597CAA282BA67110860_1708638917011_annotely_image+32.png)

 1. **All posts**: Redis returned all cached posts in 193 milliseconds.
![Allpost-with-caching](https://paper-attachments.dropboxusercontent.com/s_BE21C680105540F351BAC7689089928A0CFFE2B8C3A67597CAA282BA67110860_1708553952864_annotely_image+23.png)



2. **Single post**: It took Redis 193 milliseconds to return a single cached post. 
![Single-post-with-caching](https://paper-attachments.dropboxusercontent.com/s_BE21C680105540F351BAC7689089928A0CFFE2B8C3A67597CAA282BA67110860_1708554400719_annotely_image+26.png)


## Conclusion
In this guide, you have learned what caching is, its benefits, concepts and how to integrate Redis as a caching solution in a Node.js application. However, we used a small-sized application to illustrate how caching works, so the data retrieval time was not too significant. In larger applications, caching can make a considerable difference in performance. 

This guide also discussed the significance of selecting the right caching policy that fits into your application to minimize the load on your backend systems. It also stressed the importance of integrating cache expiration and invalidation mechanisms to ensure data accuracy and freshness.

By leveraging Redis, developers can significantly reduce response times and minimize database loads. Caching not only improves the speed of data retrieval but also reduces the workload on the server, leading to enhanced scalability and efficient resource utilization. 

As your application grows, understanding and implementing caching with Redis in Node.js becomes a valuable skill. By following the steps outlined in this guide, you can optimize your application, deliver faster response times, and ultimately provide a more satisfying and seamless user experience. 

I hope this guide was easy for you to follow. Happy Caching! 😊


## Further Reading 
- [Redis Documentation](https://redis.io/docs/)

## Resources 
- The complete code can be found [here](https://github.com/backendbro/caching-in-action-complete)