Implementing a Simple Redis Cache With Next.js
Create a simple Next.js App that connects to HealthCare.gov API optimizing the experience with Redis Cache
For most websites, the changing pieces don’t actually vary that often. That immutability makes them first-rate candidates to be cached to avoid the roundtrip to fetch the content from either a database, web service or third party end-points.
Consider a blog article that you wrote. It will be rather unlikely that this article will change over time more than once or twice, for instance, to adjust outdated information or to add a correction suggested by a reader. However, in a regular scenario, every time a visitor hits that blog page, your database will also get a hit. If your page gets 100,000 visitors, then you will get that many database-reads. And, granted, reads are generally cheap, however, in a scenario of high-performance, high-availability, each millisecond counts.
Furthermore, if your data comes from a third-party solution –say a remote web service– then each roundtrip to access the information you require will add up, in terms of network bandwidth, how many concurrent connections your remote server can handle, or even just dealing with an error on a system that you can’t debug or fix quickly.
Having an in-memory cache database can definitely help in optimization, unexpected datastores behaviors, consistency, and even save you a few bucks.
Pitfalls to keep in mind before implementing an in-memory cache
At this point, I must make the caveat that there are a few pitfalls that you need to be aware of before going full throttle with Redis, Memcache or any other in-memory database alternative. Please review the following list of potential pitfalls:
- Cache invalidation is, still today, one of most challenging tasks there is in computer engineering. The other two, according to Jeff Atwood from CodingHorror and founder of StackOverflow.com are naming things and off-by-one errors. Really, it can potentially make you lose hours on a wild goose chase. For this, I recommend having a strong game plan (cache coherence protocol), a cache expiration strategy, avoid bloating your cache with unnecessary data (such as volatile information), and finally having a periodic cache validator tuned in such a way that makes sense for your product and traffic.
- Be on the lookout for extra-greedy caching. This one is somewhat related to the previous and the next item. In essence, you want to be 100% in control of what chunks you are caching and for how long you will be storing said cache. A sporting event that happened last week will have the same score forever, it will not change. So you can, with confidence, store it and cache it. However if the game is live and happening right now you need to be able to update the score often, unobtrusively, probably not caching at all.
- Not every project is a good candidate for caching. Ideally, you want to be very careful and strategic on what sections of your site you will be caching. It makes total sense to cache an article, while it would be a really bad idea to cache a comments section, private user information such as passwords or credit cards. As mentioned above, cache invalidation is hard as it is, and you do not want to go down the rabbit hole trying to figure out why your page is not refreshing properly.
- Avoid storing complex structures. While Redis is more than capable of storing Lists, Set, Hashes, and even Streams (among others), you may want to adhere to the KISS principle. As you introduce more intricacy within your data structures you will end up defeating the purpose of having a super fast off-the-shelve data from an in-memory database. Keeping it at key-value construction will generally pay off for what we want.
- Maintenance. This one is true to practically every software you install and let live in the wild of the public Internet. You are going to need someone with a fair level of expertise on how to implement and maintain your system. Nevertheless, I should also mention that Redis is very well documented and creating a single node in-memory cache database is generally straight forward.
Having said the above, let’s try and create a very simple Next.js application that connects to HealthCare.gov open API.
Step 1: Creating a simple Next.js Application
Using Next.js as our engine to connect to an API is quite a wise decision, since we can take advantage of its server-side rendering to fetch the results to serve a fully hydrated page.
Let’s start with an empty standard project:
npm init -y
Next, we’ll get Next.js and React dependencies:
npm i -S next react react-dom
If your project runs with Typescript, install all the necessary libraries and types here as well. For the sake of simplicity we’ll make it non-Typescript, however the example could be transformed easily.
For the bare minimum Next.js project to run we need an index page. Create an index.jsx file under src/pages/index.jsx
.
For starters, let’s drop this code in this index file:
Then to keep track of how your page is going you’ll need to add a script to call next dev
from your package.json
, like this:
...
"scripts": {
"dev": "next dev"
},
...
Now, if all goes well, you can run this command from the console:
npm run dev
And after a few seconds you should see this show up in the console: event — compiled successfully
. This is your queue that you can go to your browser and navigate to http://localhost:3000. The output should be like this:
At this point you have a working environment of Next.js with a very simple home page. From here we will be using Next.js’s getStaticSideProps
function to query the HealthCare.gov API. Let’s get these logic in place.
We will create an uncomplicated articles index in a regular HTML Table. For now let’s add the skeleton of our app using with these placeholders in our index file:
We will be using Next.js built-in tools for performance measurement. This is very easy to accomplish by creating a custom app file. For this create a file src/pages/_app.jsx
and add the following content:
Now if we reload our index page in a browser you’d see this:
Not very exciting at this point. However, one important thing to notice is, in the console, you’ll see the item value: 15.56
, which defines how long it took for the page to hydrate on Next.js. We will be referring to this value to measure the success of our performance optimizations. This number is expressed in milliseconds.
This page loads very fast but, of course, this is only a static page, no remote server fetching is involved at this point. Let’s move and interact with HealthCare.gov API.
Step 2: Connecting to a remote API: HealthCare.gov
HealthCare.gov has a very simple open API that allows us to fetch their content and, according to their documentation, “so that innovators, entrepreneurs, and partners can turn it into new products and services”. Let’s do that.
We will connect to an end-point that will return a list of recent articles from HealthCare.gov. URL for this API is https://www.healthcare.gov/api/articles.json. The returned document is of type JSON, which is great news since Next.js understands JSON out of the box.
Additionally, we will install a new package, react-uuid, to uniquely identify iterative items within JSX loops, it uses 128-bit numbers.
npm i -SD react-uuid
Let’s rewire our index.jsx
file as follows:
A few things are happening here. getStaticProps
will fetch the information needed for the page before sending it to the client. This function also calls another function, fetchData
, which is the one that does the heavy work of actually connecting to the API.
The other thing that is worth mentioning is that now the Home
component receives a props
argument, which is de-structured into a single articles
variable. Then, this variable is used and iterated through in the returned JSX code. Notice the uuid()
method call that will guarantee the uniqueness of each row when we run the loop.
Then, our rendered page will end up looking like this:
Aha! We now have some real articles rendered. Notice our hydration value ballooned up to 53.49. This is, in all fairness, really fast, but let’s be clear that this is almost 4 times our previous static page value.
It would make sense to store this response and keep it for a sensible amount of time so that we don’t have to request it from HealthCare.gov servers on each page reload. Let’s throw Redis into the mix.
Step 3: Installing Redis
Redis is generally used as an in-memory cache for fast access to key-valued data. But Redis is so much more. It can also be used as a regular database, as a message broker, and even can work in replication in a cluster. For simplicity sake, in this article we will use Redis as an in-memory cache storage where we will save the responses from HealthCare.gov API.
A very easy way to install Redis is with Docker containers. Why? You only need to run this command:
docker run -p 6379:6379 --name my-redis -d redis
And you’re all done. You have a running instance of Redis locally and accessible at port 6379.
Of course, be mindful that, if you kill the my-redis
container, you will lose your data. This is not a big deal at the moment, since we are using Redis as an in-memory cache, meaning that in it we will be storing volatile data only.
Step 4: Caching the API results
The fun part is next. We will make our running Redis instance act as a “middle man” between HealthCare.gov API and our app. Before fetching data remotely, our app will confirm with Redis if the data is already living locally in-memory. This operation is often called checking for a ‘cache hit-miss’.
Let’s kick it off by installing a Redis npm package, there are a bunch around. The one we are going to use is the one plainly called redis. Also we are installing bluebird, a library that allows us to ‘promisify’ all redis methods (redis npm package will support promises in version 4).
npm i -S redis bluebird
So, we need to update our index file like this:
Most exciting things are happening inside the getStaticProps
method. With bluebird.promisifyAll
we effectively create shadow methods with the ending -Async, plus all of them return a promise now. So, cache.get()
now becomes cache.getAsync()
, additionally you can attach the await
keyword in front of it. Again, when redis packages version 4.0 gets released, you can skip this step.
And then, a little logic which just checks if the key exists in our cache database; if it doesn’t (a cache miss) then we go and fetch it from the HealthCare.gov API servers. If the key is found (a cache hit), then we just go ahead and get that value, without fetching data from the remote server.
A little bit JSON.stringify
and JSON.parse
acrobatics are needed, since we are storing the JSON response from the API as a plain string.
The output in the browser should be exactly the same:
Notice how the first time you will be connecting to the remote API, subsequent reloads will not connect to the remote server and will be using the cached version of the response JSON.
In this particular example, we got a response with a Next.js hydration value of 30.57, which almost cuts in half the total hydration time when connecting to the remote server. Now, this is a very simple, almost naïve example, so there will be a big margin of variance in our hydration time. For a more robust and complete application the optimization and time saving will be more visible.
This is all good and works, however, what if HealthCare.gov publishes a new article? Unfortunately, with our setup, this article will not be seen in our app, since our cache prevents us from refreshing our articles list with new publications. It is at this point that you want to try some good-ole cache invalidation techniques.
Step 5: Invalidating cache results
As mentioned earlier, cache invalidation is one of the most challenging tasks you could encounter in computer science. There are several techniques, one more complex than the previous. For our case, we will go with a simple cache expiration technique.
For this, we will use a new key in our Redis database, to avoid messing up with the previous articles
key. For this example we will use exp-articles
.
The other adjustment we need to do is to add the extra 'EX'
and 60 * 60
as the third and fourth parameters of the cache.set()
method. The 'EX'
parameter just flags that this key will have an expiration date, while the last one multiplies 60 seconds by 60, in other words, this key will expire in 1 hour.
With this, we have made it so that our cached key will expire every hour, and so, we will have fresh content from the remote server constantly. This expiration number should be a value that makes sense for your business without compromising the integrity of the content or the cache lifespan.
Additional optimization tasks
While you can store binary data in a Redis Database –which means you can cache images, scripts, PDF files–, you may want to use a specific tool to cache binary files, like as a high performance HTTP Accelerator, such as Varnish. If you use Varnish to speed up your binary files HTTP transfers, you can see improvements up to hundreds of times in faster downloads:
“Varnish Cache is really, really fast. It typically speeds up delivery with a factor of 300–1000x, depending on your architecture.”
Having Redis Store targeted sections of your site –articles, quick relatively volatile statistics, session management– while at the same time implementing a Varnish Cache to return your assets –images, almost-static scripts or stylesheets, SVG graphics, videos– will more likely make your site extremely fast and available.
Another improvement you can make is that, both Redis and Varnish can work in clusters, meaning you can have several instances of your in-memory database. A common practice is to have one or several read-only instances while updates and writes will be handled by a master instance.
Given the current cloud-based-serverless world we are living in now, it would be fairly easy to kick-off a Redis Databases farm, dockerize each instance and allow them to be privately accessed by your app or web services.
What’s Next and Further Reading
Learn more about performance and speeding up your website:
Further reading: