What is caching, and why is it important?
So what is caching exactly?
It's a process where we store copies of frequently used data in storage with faster access times. Whenever the application needs this data, it will first check whether it's available in the cache. If the data is unavailable, it will try to retrieve it from the source. This way, we can save a lot of time on expensive calculations, network traffic, or load times. Caching is a very versatile approach used everywhere, from the lower levels, such as CPU caching and operating systems, to web apps.
In the rest of this article, we'll explore how you can leverage this to improve performance and user experience, but also what to look out for when setting up caching for your application and the potential risks.
The first type of caching we'll look at is HTTP caching, including browser and proxy caches and CDNs (if you still need to become more familiar with what HTTP is, you should check out our previous article on HTTP requests).
All modern web browsers support this, allowing them to store some of the resources sent by the server (e.g., images, stylesheets, scripts, and so on) locally instead of fetching them from the server every time the user visits the website.
As this has multiple advantages:
- Faster loading time. Larger resources (images, for example) might take some time to download, increasing the page's loading time. By caching them, we can avoid this issue.
- Since we are not getting the resources directly from the server every time the user visits the page, this also reduces the server load.
- Reduced network traffic. This is especially useful for users who are on a limited data plan.
To control what resources should be cached and when they should be updated, we have access to a few headers:
The Cache-Control header holds instructions (called directives) that control how caching should be done. Some of those directives include max-age, which controls for how long the cache is valid, or no-store, which completely disables the caching for the specific request. You can check out this MDN web docs page for a complete list of these directives.
You can think of the ETag as an identifier of a specific version of a resource, so when the resource changes, so will the ETag. By comparing the ETags, we can check whether there are any updates without requesting the entire resource; if there aren't, we can reuse the data we already have. This allows us to be much more efficient.
This header stores the time when the resource was last updated. It serves a similar purpose as the ETag.
Another type of HTTP caching is proxy caching. Here, instead of storing the data on the user's device, it's stored on a proxy server between the client and the application server. When the server sends a response to a specific request, the proxy will intercept it and store it locally in its cache.
Next time a client requests the same resource, the proxy will be able to return the cached response without having to forward the request to the application server. Like with the browser cache, here we can use the same HTTP headers to control how the caching works.
Unlike the browser cache, the one in the proxy is used for all the clients' requests, so this type of caching is sometimes called public. On the other hand, the browser cache is available only for one specific client, so it's a private cache.
CDNs (Content Delivery Networks) will cache content on geographically distributed servers to be closer to most users, which improves the web application's performance and makes it more reliable, as the content is stored on multiple servers.
Server-side caching is used to improve the server's performance and reduce the load on the server by storing frequently used data. This can be API responses, results from database queries, or results from any other time-consuming operations.
There are many different ways to implement server-side caching. Some of them are:
- In-memory caching. We can access data quickly by storing it in the server's memory. This is most suitable for smaller data sets that must be accessed frequently. We can implement in-memory caching either with libraries such as node-cache for Node.js for smaller projects or by using in-memory databases like Redis, which can provide a centralized cache if you ever need to scale to multiple servers.
- Storing the data in the server's file system; is usually used for bigger data that don't need to be accessed that often.
- Database caching, where the results of frequently used queries are stored either in memory or on the disc.
What to look out for when implementing caching?
There are a few potential problems you could run into when implementing caching, including:
- If the cache gets too big, it can negatively affect the application's performance.
- Storing incorrect or outdated data in the cache.
- Security problems. As we saw in previous examples, sometimes cached data can be accessed by multiple clients. It's important not to include any user-specific or otherwise sensitive data in the cache.
To avoid these issues, we must carefully plan what data should be cached and how long it should be stored before the data is replaced or invalidated.
And in case you are working on more extensive multithreaded applications, here's a bonus one: cache stampede.
As we said earlier, in server-side caching, if the requested data is not present in the cache or has expired, the request will get it from the database instead. So, we have thousands of requests trying to access the missing data simultaneously. In that case, they will all be forwarded to the database, overloading it and bringing down the entire system in the worst-case scenario.
There are multiple ways to prevent this:
- External computation: keeping an external process that will re-compute the cache values on specific intervals or when a cache miss happens
- Probabilistic early expiration: the resource is re-calculated just before it's about to expire
- Locking: While one of the threads is re-calculating the value, the others are locked out. Here, it's up to you to decide whether they will have to wait or get stale data.
Caching is a powerful tool that can help us improve our web applications' performance and user experience. Still, it's important to always carefully plan how we will implement it to achieve the best results and avoid any unwanted consequences.