Back to Blog
Tutorial13 min read2026-08-02

Speed Up NestJS with Managed Redis Caching

Add a Redis-backed cache to your NestJS API using PandaStack's managed KV store. Automatic KV_URL injection, ioredis client setup, and cache invalidation patterns.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

A NestJS API that queries PostgreSQL for every request is slower than it needs to be. User profiles, product catalogs, and configuration data rarely change, but fetching them from the database takes 10-50ms per query. A Redis cache reduces that to under 1ms by keeping frequently accessed data in memory.

PandaStack's KV store is a managed Redis instance. You provision it via the API, link it to your NestJS project, and get KV_URL injected as an environment variable. Your application code connects to Redis and caches expensive database queries, API responses, or session data.

Provision the KV store

Create a Redis instance via the REST API:

curl -X POST https://api.pandastack.io/v1/kv \
  -H "Authorization: Bearer $PANDASTACK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "api-cache"}'

The response includes a kvId. Save it — you'll link it to your NestJS project.

Deploy the NestJS app and link the KV store

Create a project and link the KV store in one request:

curl -X POST https://api.pandastack.io/v1/projects \
  -H "Authorization: Bearer $PANDASTACK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "container",
    "name": "nestjs-api",
    "repositoryName": "acme/nestjs-api",
    "branch": "main",
    "autoDeploy": true,
    "kvId": "<your-kv-id>"
  }'

PandaStack builds your NestJS app and injects KV_URL, KV_REST_API_URL, and KV_REST_API_TOKEN. The KV_URL is a standard Redis connection string; the REST API variables are for HTTP-based access (Upstash-style).

Set up ioredis in NestJS

Install the Redis client:

npm install ioredis

Create a Redis module in src/redis/redis.module.ts:

import { Module, Global } from '@nestjs/common';
import Redis from 'ioredis';

const redisProvider = {
  provide: 'REDIS_CLIENT',
  useFactory: () => {
    return new Redis(process.env.KV_URL);
  }
};

@Global()
@Module({
  providers: [redisProvider],
  exports: ['REDIS_CLIENT']
})
export class RedisModule {}

Import it in app.module.ts:

import { Module } from '@nestjs/common';
import { RedisModule } from './redis/redis.module';

@Module({
  imports: [RedisModule],
})
export class AppModule {}

Now any service can inject the Redis client:

import { Injectable, Inject } from '@nestjs/common';
import Redis from 'ioredis';

@Injectable()
export class UsersService {
  constructor(@Inject('REDIS_CLIENT') private redis: Redis) {}

  async getUserById(id: number) {
    const cacheKey = `user:${id}`;
    const cached = await this.redis.get(cacheKey);

    if (cached) {
      return JSON.parse(cached);
    }

    const user = await this.fetchUserFromDatabase(id);
    await this.redis.set(cacheKey, JSON.stringify(user), 'EX', 3600);
    return user;
  }

  private async fetchUserFromDatabase(id: number) {
    // Query PostgreSQL or call an ORM
  }
}

The first request for user 42 hits the database, then caches the result for 1 hour (3600 seconds). Subsequent requests return the cached value in under a millisecond.

Cache invalidation

When a user updates their profile, the cache becomes stale. Invalidate it explicitly:

async updateUser(id: number, data: any) {
  await this.database.update(id, data);
  await this.redis.del(`user:${id}`);
}

This deletes the cached entry, so the next read fetches fresh data from the database.

Use Redis for session storage

NestJS session middleware can store sessions in Redis instead of memory:

npm install express-session connect-redis

Configure it in main.ts:

import * as session from 'express-session';
import * as connectRedis from 'connect-redis';
import Redis from 'ioredis';

const redisClient = new Redis(process.env.KV_URL);
const RedisStore = connectRedis(session);

app.use(
  session({
    store: new RedisStore({ client: redisClient }),
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: { maxAge: 86400000 }
  })
);

Now sessions persist across pod restarts and work correctly when your app scales to multiple replicas.

HTTP-based Redis access

If you prefer HTTP over a native Redis connection, use the REST API:

async getCachedValue(key: string): Promise<string | null> {
  const response = await fetch(
    `${process.env.KV_REST_API_URL}/get/${key}`,
    {
      headers: {
        'Authorization': `Bearer ${process.env.KV_REST_API_TOKEN}`
      }
    }
  );

  const data = await response.json();
  return data.result;
}

This works from edge functions or environments where you can't use native Redis clients, but it's slower than a direct TCP connection.

Deploy with pandastack.json

Declare the KV dependency in pandastack.json so the configuration is version-controlled:

{
  "type": "container",
  "language": "nodejs",
  "startCommand": "npm run start:prod",
  "healthCheckPath": "/health",
  "env": [
    { "key": "KV_URL", "description": "Managed Redis connection string (auto-injected)" }
  ]
}

When someone deploys this repo, the dashboard prompts them to link a KV store. This makes the project forkable: contributors can spin up their own Redis instance for testing.

Monitor cache hit rates

Add logging to track how often the cache is used:

async getUserById(id: number) {
  const cacheKey = `user:${id}`;
  const cached = await this.redis.get(cacheKey);

  if (cached) {
    console.log(`Cache hit: ${cacheKey}`);
    return JSON.parse(cached);
  }

  console.log(`Cache miss: ${cacheKey}`);
  const user = await this.fetchUserFromDatabase(id);
  await this.redis.set(cacheKey, JSON.stringify(user), 'EX', 3600);
  return user;
}

Check the logs (Dashboard → Logs) to see the hit/miss ratio. A high miss rate means either your TTL is too short or the keys aren't being reused.

Use the CLI for local development

Run a local Redis instance with Docker:

docker run -d -p 6379:6379 redis:7
export KV_URL=redis://localhost:6379
npm run start:dev

Your NestJS app connects to the local Redis, so you can test caching without deploying.

Debugging connection failures

Error: connect ECONNREFUSED: KV_URL is missing or malformed. Check the environment variables in the dashboard.

Error: getaddrinfo ENOTFOUND: The Redis hostname is wrong. Verify the KV store is linked to the project.

ReplyError: READONLY You can't write against a read only replica: You're connected to a read replica by mistake. PandaStack doesn't expose replicas yet, so this shouldn't happen — file a support ticket.

redis-cli can't connect: The KV store requires TLS with SNI, and redis-cli doesn't send SNI by default. Use ioredis in your app, or connect via the REST API instead.

Scale the KV store

Free-tier KV instances have a storage limit (exact size depends on the plan). If your cache exceeds it, Redis evicts the least-recently-used keys. For production apps, upgrade to Pro ($15/mo) for more capacity.

Cache expiration strategies

Set a TTL (time-to-live) for every key to prevent the cache from filling with stale data:

  • Short TTL (5-60 seconds): Frequently changing data like stock prices
  • Medium TTL (10-60 minutes): User profiles, product catalogs
  • Long TTL (hours or days): Static configuration, rarely updated content

If a value never changes, cache it indefinitely and invalidate manually when it updates.

What about PostgreSQL query caching?

PostgreSQL has its own query cache, but it only helps with identical queries. If your query has dynamic parameters (e.g., SELECT * FROM users WHERE id = ?), each ID is a separate query. Redis caches the result by user ID, so you get sub-millisecond lookups regardless of query parameters.

References

  • [ioredis documentation](https://github.com/redis/ioredis)
  • [NestJS caching](https://docs.nestjs.com/techniques/caching)
  • [Redis best practices](https://redis.io/docs/manual/patterns/)
  • [PandaStack KV store](https://docs.pandastack.io/kv)

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also