Marcio Cunha

Slow API under heavy load? How to implement tiered caching

Learn how to combine local memory and Redis to relieve database load and speed up critical endpoints under high traffic volumes.

Marcio Cunha•4 min
Also available in:EspañolPortuguês
Summary
  • The tiered caching strategy combines the raw speed of local memory with the resilience of a distributed key-value store.
  • Using Redis prevents simultaneous requests from crashing the primary database during heavy traffic spikes.
  • Data invalidation becomes the primary architectural challenge when decentralizing temporary storage.
  • Implementing a structured read and write pattern drastically reduces the latency perceived by the end user.
  • Maintaining clear expiration policies ensures outdated information quickly disappears from servers.

The invisible bottleneck of high-volume APIs

When a system starts receiving thousands of requests per second, the database is usually the first component to show signs of strain. Repetitive queries searching for the exact same information consume precious CPU cycles and disk I/O. In practice, this means a large portion of computational effort is wasted answering questions whose answers haven't changed since the previous second. To solve this problem without having to buy larger servers right away, modern architecture relies on temporary data storage.

In intense traffic scenarios, relying on a single query layer is an invitation to slowness and instability. It is precisely in this context that the tiered caching strategy stands out, dividing the workload between the application's own memory and an external distributed service. If you have already noticed that your API needs breathing room during traffic spikes, understanding how to organize these storage tiers is the first step toward restoring system performance.

How the division between local memory and Redis works

The concept behind tiered caching is simple to visualize if we think of an office desk setup. The application's local memory acts like the notepad right in front of you, accessible in fractions of a microsecond. Meanwhile, Redis acts as a shared drawer with the whole team, slightly further away, yet extremely fast and accessible by any server in the cluster. This separation allows handling the vast majority of requests without even touching the network.

When a request hits a critical endpoint, the server first checks its own RAM. If the data is there, the response is delivered instantly. Otherwise, the application queries Redis, which stores records centrally for all API instances. Only if the data is nowhere to be found in these two places is the primary database queried, repopulating the upper tiers for subsequent calls. This hierarchy ensures fast responses even when the infrastructure undergoes sudden load variations.

Operational challenges and the complexity of invalidation

Splitting storage into multiple levels brings a brutal speed gain, but it introduces a classic computing problem: data consistency. When information is updated in the primary database, all copies scattered across servers' local memory and Redis must be invalidated or updated. Otherwise, the user will continue seeing old data for minutes or even hours, which can cause critical failures in transactional flows and e-commerces.

To overcome this obstacle, approaches based on short expiration times or publish-subscribe events are used. In the first scenario, a tolerable delay in data updates is accepted by letting items expire on their own after a few seconds. In the second, each database change triggers a message instructing nodes to clear their local records immediately. The secret lies in calibrating this lifespan according to information criticality, balancing response speed with business precision.

Implementing the strategy in practice with code

Structuring code to handle tiers requires clarity in search and save routines. Below is a conceptual Node.js example demonstrating how to check local memory first, then Redis, before resorting to the final relational database.

const NodeCache = require('node-cache');
const localCache = new NodeCache({ stdTTL: 10 });
const Redis = require('ioredis');
const redis = new Redis();

async function getProductData(productId) {
  const localKey = `product:${productId}`;
  let data = localCache.get(localKey);
  if (data) return { source: 'local', data };

  const redisData = await redis.get(localKey);
  if (redisData) {
    data = JSON.parse(redisData);
    localCache.set(localKey, data);
    return { source: 'redis', data };
  }

  data = await fetchFromDatabase(productId);
  await redis.set(localKey, JSON.stringify(data), 'EX', 60);
  localCache.set(localKey, data);
  return { source: 'database', data };
}

The snippet above illustrates the path taken by a successful query. Notice how the application tries the fastest path first, saving the result in upper layers as it progresses. This pattern prevents bottlenecks and protects centralized storages against sudden access spikes, maintaining overall microservices architecture stability. To complement this implementation in production environments, see our related technical analyses on AI generating vulnerable code? How to apply custom SAST and API crashing? How to implement Rate Limiting with Redis.

Final considerations on resilience and performance

Adopting a tiered caching architecture goes far beyond speeding up responses; it is about building a resilient system capable of absorbing unexpected traffic without degrading user experience. Although consistency management brings additional development challenges, the benefits in terms of scalability and resource savings amply outweigh the complexity. When planning your critical routes, remember that the best database query is the one that never gets executed.