Marcio Cunha

Slow API in NestJS? How to apply Redis distributed cache

Learn how to eliminate latency in critical NestJS endpoints by applying advanced distributed caching and invalidation strategies with Redis.

Marcio Cunha•4 min
Also available in:PortuguêsEspañol
Summary
  • Caching drastically reduces direct database access in high-read endpoints
  • Redis acts as a fast shared memory layer across multiple application instances
  • Event-driven invalidation prevents stale data and maintains record consistency
  • Efficient object serialization optimizes memory usage and accelerates cluster lookups
  • Custom interceptors in NestJS automate the cache lifecycle without polluting controllers

Why slow APIs demand a distributed caching strategy

When a web application grows, the database is usually the first component to suffer under the weight of repetitive queries. If hundreds of users request the same information at the same time, the API starts slowing down and the user experience plummets. In practice, this means the server wastes time processing data that hasn't changed since the last request. To solve this scaling problem, we need an intermediate layer that stores ready-made responses and delivers data in fractions of a millisecond.

In modern architectures built on microservices or multiple servers running in parallel, keeping data only in the RAM of a single machine stops working well. If a user hits server A, they see updated data, but if the next request lands on server B, the information might be outdated or missing. This is precisely where distributed cache comes in, an external shared memory that all application instances can query and update in a centralized and synchronous manner.

The role of Redis in speeding up critical NestJS endpoints

Redis is an extremely fast in-memory database that operates through key-value pairs, ideal for storing temporary data with top-tier performance. By integrating it with NestJS, a modular and organized framework for Node.js, we can create elegant workflows to intercept requests before they reach complex business logic. In practice, NestJS receives the HTTP call, checks if the corresponding key already exists in Redis, and if it is there, returns the response immediately without querying PostgreSQL or MySQL.

To connect Redis to NestJS cleanly, we use mature libraries like ioredis combined with the platform's native cache module. This allows setting automatic expiration times, known as TTL or Time to Live, ensuring data expires on its own after a few minutes to prevent old information from getting stuck forever. When we structure this layer alongside optimized communication between microservices, the performance boost in critical endpoints is immediate and measurable.

How to implement custom cache interceptors in NestJS

NestJS makes manipulating the request lifecycle very easy through interceptors, which act as intelligent filters capable of modifying the input and output of an endpoint. We can create a dedicated interceptor that examines the called route, generates a unique key based on URL parameters, and queries Redis even before the controller executes. If the data doesn't exist, the interceptor lets the request proceed, captures the result returned by the database, saves it in Redis, and only then delivers it to the client.

Below is a practical example of a TypeScript interceptor that performs this check transparently on any route where it is injected:

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import Redis from 'ioredis';

@Injectable()
export class RedisCacheInterceptor implements NestInterceptor {
  private redis = new Redis();

  async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
    const request = context.switchToHttp().getRequest();
    const cacheKey = `cache:${request.url}`;

    const cachedData = await this.redis.get(cacheKey);
    if (cachedData) {
      return of(JSON.parse(cachedData));
    }

    return next.handle().pipe(
      tap(async (response) => {
        await this.redis.set(cacheKey, JSON.stringify(response), 'EX', 60);
      }),
    );
  }

This code drastically reduces the workload on the main server, but it introduces a classic software engineering challenge: what happens when the original data changes in the database?

Advanced invalidation strategies to prevent ghost data

Caching information is easy, but ensuring users never see outdated data after a modification is the true test of a system's maturity. When a record is updated or deleted, the corresponding key in Redis must be immediately invalidated or updated, otherwise the system will keep serving the old version. In practice, we call this event-based invalidation, where every write operation to the database triggers a command to clear the stale key from Redis.

A robust approach consists of publishing state change events through an internal bus or messaging system, similar to what happens when we structure a resilient architecture with a circuit breaker to protect dependent services. This way, we decouple business logic from cache cleaning, ensuring that even if the database suffers write spikes, the consistency of data returned by the API remains intact and reliable for clients. To explore these architectural concepts further in production, see our related technical analysis on Unstable AI? How to use DSPy to structure prompts.

Final thoughts on performance and scalability

Applying distributed caching with Redis in NestJS transforms an application's handling capacity, reducing response times from hundreds of milliseconds to mere microseconds. However, this convenience requires discipline in key naming, correct expiration times, and rigorous invalidation routines to prevent hard-to-track bugs. By combining these practices with constant memory usage monitoring, your API will be ready to scale safely under any traffic volume.