Marcio Cunha

Slow API? How to speed up REST endpoints with memory cache

Learn how to structure efficient invalidation strategies and layered storage to eliminate latency in microservices and REST APIs.

Marcio Cunha•5 min
Also available in:PortuguêsEspañol
Summary
  • Temporary in-memory storage drastically reduces the computational effort required by the underlying relational database.
  • Event-driven invalidation ensures outdated data is purged without sacrificing overall application performance.
  • Layered caching distributes the read load between local application memory and centralized cache servers.
  • Strict time-based expiration policies prevent stale responses from lingering indefinitely in the system.
  • Continuous monitoring of cache hit and miss rates prevents unexpected operational bottlenecks in production.

Why your API is slow and the role of caching

When a web application grows, the volume of database queries scales at the exact same pace. In distributed microservice architectures, every unnecessary query means precious milliseconds lost across the network and internal processing. In practice, this means users wait seconds for responses that could be delivered instantly if the system did not recalculate everything from scratch on every click. The most direct solution to this bottleneck is caching, which involves keeping copies of frequently accessed data in ultra-fast locations, such as the computer's RAM.

To understand the real impact, imagine a service desk where an employee has to dig through dusty basement archives every time someone asks a simple question. In software development, the hard drive or database server acts as that basement, while main memory acts as the desk where the most important papers are ready for immediate use. Applying this logic to REST endpoints avoids unnecessary trips to the persistence layer and drastically speeds up the delivery of static or semi-static data. For those looking to optimize infrastructure resources in complex scenarios, this strategy complements other performance fronts, just as we optimize hardware resources by setting up local graphics cards for heavy workloads.

How layered storage works in practice

Putting all storage responsibility into a single place often creates single points of failure or severe concurrency bottlenecks. That is why modern architectures adopt layered storage, combining local cache in the application process's own memory with a centralized distributed cache, such as Redis or Memcached. Local cache is extremely fast because it requires no additional network calls, but it consumes the RAM of the specific server running that API instance. On the other hand, distributed cache shares saved data across all system instances, ensuring overall consistency even when traffic spreads across dozens of servers.

In practice, the user request first hits the microservice and checks if the data exists in local memory, operating in nanoseconds. If a cache miss occurs, the application queries the centralized distributed cluster. If the data is still not there, the database is queried, the response is calculated, and it is propagated back up to the upper layers. This cascading model protects the infrastructure against sudden traffic spikes and drastically reduces CPU consumption during repetitive queries. This same concern for efficient memory and processing usage mirrors the care we take when structuring internal microservice communication in a lightweight manner.

Smart data invalidation strategies

The biggest challenge of working with temporarily saved data is not creating the storage, but knowing the exact moment to discard it when the original information changes. If a user updates their profile and keeps seeing old data due to a frozen response in memory, trust in the system plummets. To solve this problem, approaches known as event-driven invalidation and time-to-live expiration, commonly referred to as TTL, are used. TTL defines a maximum validity period for the record, making it expire automatically after a few minutes or seconds, depending on content volatility.

On the other hand, systems requiring immediate consistency use active invalidation triggered by modification events, such as a database update publishing a message to an internal bus telling all instances to clear that specific key. The following code illustrates a simple example in Node.js using a middleware to intercept GET requests and check in-memory storage before querying the main route:

const cache = new Map();

function cacheMiddleware(durationInSeconds) {
  return (req, res, next) => {
    const key = req.originalUrl;
    const cachedResponse = cache.get(key);

    if (cachedResponse && Date.now() < cachedResponse.expiresAt) {
      return res.json(cachedResponse.data);
    }

    res.sendResponse = res.json;
    res.json = (body) => {
      cache.set(key, {
        data: body,
        expiresAt: Date.now() + (durationInSeconds * 1000)
      });
      res.sendResponse(body);
    };
    next();
  };
}

Common pitfalls and essential design precautions

Implementing temporary storage without planning can introduce hard-to-track bugs, such as unbridled RAM consumption leading to sudden crashes due to resource exhaustion. When the number of unique keys grows indefinitely without an eviction policy, the server runs out of available memory and the entire system stops responding. To prevent this type of collapse, it is crucial to configure exclusion policies known as LRU, which stands for Least Recently Used, automatically removing the least recently accessed items when the space limit is reached. Additionally, storing sensitive or user-specific data in shared global caches without proper key segmentation by security identifiers must be avoided.

Another frequent mistake is ignoring the impact of the cache stampede effect, which occurs when a very popular key expires and tens of thousands of concurrent requests hit the database at the same time to recalculate the exact same information. To mitigate this issue, advanced techniques like distributed locks or background revalidation ensure that only one request recalculates the data while the others receive a slightly older version for a few moments. With a well-designed cache architecture aligned with actual user behavior, your application gains the necessary robustness to scale without sacrificing response speed.

Final considerations

Accelerating REST endpoints through memory caching and layered storage is not just an aesthetic optimization detail, but a fundamental requirement to ensure the stability and longevity of modern systems. By balancing local and distributed memory usage with clear invalidation and expiration policies, engineering teams can shield their databases against unwanted traffic spikes. The secret to success lies in understanding your business's specific characteristics to choose the exact right moment to expire or update each stored piece of information.

Investing time in planning these strategies transforms the end-user experience and optimizes cloud infrastructure operating costs definitively. Adopting best practices from the beginning of development avoids future rework and prepares your application to grow sustainably and predictably under any load scenario.