Pythonium

Python, What else ?

Why does caching create so many bugs?

When trying to optimize an application's performance, one of the first things we think about is setting up a cache. However, it is also an endless source of bugs that are often difficult to reproduce. I have already struggled countless times with cache-related issues.

Who has never heard a developer say: "It works on my machine." While for a user, the application is still displaying an old version of a page or outdated data?

The problem is usually not the cache itself, but the fact that there are multiple cache layers interacting with each other. When a bug appears, it can be difficult to know which one is responsible.

Why use a cache?

A cache avoids recalculating or downloading data that is already known. For example:

  • an image downloaded once can be kept by the browser;
  • an HTML page can be served directly by a CDN;
  • an expensive SQL query can be stored in Redis.

The goal is always the same: reduce response time and save resources (and therefore money). The problem is that cached data can become outdated after an update, for example.

Browser cache

The browser is often the first source of surprises. Imagine that you fix a CSS file.

https://example.com/style.css

You deploy your new version, reload the page... and nothing changes. The browser is simply using the version it already has. HTTP headers control this behavior:

Cache-Control: max-age=86400

Here, the browser considers that the resource remains valid for 24 hours. To avoid this problem, it is common to version resources:

style.css?v=42

or:

style.3f28c9.css

With each new deployment, the URL changes and the browser automatically downloads the new version.

CDN

A CDN also has its own cache. When a user requests a resource, the CDN can respond directly without contacting your server.

This is excellent for performance and user experience, but it also means that after a deployment, the CDN may continue serving an old version. Some platforms allow manually clearing the cache ("purge"), while others simply wait for the TTL expiration.

As a result, a developer may see changes immediately while a user located in another region continues receiving an old version for several minutes or even several hours, sometimes making the application or website unusable.

Redis and application data

Redis is widely used to speed up applications.

Let's take an example. A page displays a user's profile.

Without a cache:

User
    ↓
Database
    ↓
Response

With Redis:

User
    ↓
Redis
    ↓
Database (only if necessary)

The first request queries the database and stores the result in Redis. The following requests are almost instantaneous (On Windows, I have sometimes experienced a few milliseconds of unexplained latency...).

The problem appears when a user updates their profile. If Redis still contains the old version, the application continues displaying outdated information. We will see in the following sections how to handle this.

The cache invalidation problem

In computer science, there is a famous quote:

There are only two hard things in Computer Science: cache invalidation and naming things.

From my experience, I completely agree with this quote! Although generative AIs have significantly reduced the effort required for naming things, cache invalidation is still here!

The difficulty is not storing data, but knowing exactly when that data is no longer valid and needs to be reloaded.

Let's say you cache the product list of an online store. When a product is added, deleted, or modified, should you:

  • only remove this product from the cache?
  • remove the entire category?
  • remove the entire store?
  • wait for automatic expiration?

The larger an application becomes, the more complex this logic gets. And we have not even talked about concurrency, which adds another layer of complexity. Cache invalidation in a concurrent environment can quickly become a nightmare. This is the type of problem I have struggled with the most during my developer career.

Time-to-live (TTL)

One solution is to let the cache expire automatically.

For example:

TTL = 300 seconds

After five minutes, the cache system will automatically remove the entry. This approach is simple but imperfect. During these five minutes, users may see potentially outdated data.

Conversely, a TTL that is too short greatly reduces the benefit of caching because performance improvements become limited.

Multiple caches at the same time

The most complicated situation is that the same resource can exist simultaneously in several caches.

For example:

Browser
        ↓
CDN
        ↓
Reverse Proxy
        ↓
Redis
        ↓
Database

If each one contains a different copy of the same data, identifying the source of a problem quickly becomes a nightmare, especially when the issue cannot be reproduced.

The most common bugs

Here are some typical situations:

  • an image remains outdated after a deployment;
  • a JavaScript file is updated but users are still executing the old version;
  • a user updates their profile but still sees old information;
  • an API sometimes returns an old response;
  • two users get different results for the same request.

These bugs are often difficult to reproduce because they depend on each user's cache state.

Some best practices

Some rules help limit problems:

  • version CSS and JavaScript files;
  • use appropriate Cache-Control headers;
  • define a consistent TTL;
  • explicitly invalidate the cache when important changes occur;
  • document where each piece of data is cached;
  • monitor the cache hit ratio to detect abnormal behavior;
  • do not forget about caching when managing concurrent access.

Conclusion

Caching is essential for building fast applications, and we cannot avoid it. Good luck avoiding bugs, and dealing with future production issues caused by it ;p




Laisser un commentaire