---
id: "titan/modules/redis"
title: "titan-redis"
---

# titan-redis

<span class="module-badge">official · @omnitron-dev/titan-redis · stable</span>

Redis connection management with clustering, Sentinel, TLS, multiple
named instances, pluggable Lua scripts, and the underlying foundation
for `titan-cache` (L2), `titan-lock`, `titan-discovery`,
`titan-ratelimit`, and `titan-notifications`.

```bash
pnpm add @omnitron-dev/titan-redis
```

## Quickstart

### Single client

```typescript
import { RedisModule } from '@omnitron-dev/titan-redis';

@Module({
  imports: [
    RedisModule.forRoot({
      config: { host: 'localhost', port: 6379, db: 0 },
    }),
  ],
})
class AppModule {}
```

### Multiple named clients

```typescript
RedisModule.forRoot({
  clients: [
    { namespace: 'main',   host: 'redis-main',   port: 6379, db: 0 },
    { namespace: 'cache',  host: 'redis-cache',  port: 6379, db: 1 },
    { namespace: 'queue',  host: 'redis-queue',  port: 6379, db: 2 },
  ],
  commonOptions: { maxRetriesPerRequest: 3 },
})
```

### Cluster

```typescript
RedisModule.forRoot({
  config: {
    cluster: {
      nodes: [
        { host: 'redis-1', port: 6379 },
        { host: 'redis-2', port: 6379 },
        { host: 'redis-3', port: 6379 },
      ],
      maxRedirections: 16,
      retryDelayOnClusterDown: 100,
      scaleReads: 'replica',
    },
  },
})
```

### Sentinel

```typescript
RedisModule.forRoot({
  config: {
    sentinels:    [{ host: 'sentinel-1', port: 26379 }, { host: 'sentinel-2', port: 26379 }],
    sentinelName: 'mymaster',
    password:     env.REDIS_PASSWORD,
  },
})
```

### TLS

```typescript
RedisModule.forRoot({
  config: {
    host: 'redis-tls.internal',
    port: 6380,
    tls:  { rejectUnauthorized: true, ca: [fs.readFileSync('./ca.pem', 'utf8')] },
  },
})
```

### Async configuration

```typescript
RedisModule.forRootAsync({
  useFactory: (config: ConfigService) => ({
    config: {
      host: config.get('redis.host'),
      port: config.get('redis.port'),
    },
  }),
  inject: [ConfigService],
})
```

## `RedisModuleOptions`

| Option              | Type                                                                                                  |
| ------------------- | ----------------------------------------------------------------------------------------------------- |
| `config`            | `IRedisClientOptions` — single-client config                                                          |
| `clients`           | `IRedisClientOptions[]` — multiple named clients                                                      |
| `commonOptions`     | `Partial<IRedisClientOptions>` — applied to every client                                              |
| `closeClient`       | `boolean` — close during module shutdown (default: `true`)                                            |
| `shutdownTimeout`   | `number` — QUIT acknowledgement budget in milliseconds (default: `5000`; `0` disconnects immediately) |
| `readyLog`          | `boolean` — log when each client emits 'ready'                                                        |
| `errorLog`          | `boolean` — log on connection errors                                                                  |
| `logger`            | `ILogger` — override default logger                                                                   |
| `onError`           | `(error, client) => void` — global error callback                                                     |
| `onClientCreated`   | `(client) => void`                                                                                    |
| `onClientDestroyed` | `(namespace) => void`                                                                                 |
| `healthCheck`       | `{ enabled?, timeout?, interval? }`                                                                   |
| `scripts`           | `Array<{ name, path?, content? }>` — Lua scripts to preload                                           |
| `pool`              | `{ min?, max?, acquireTimeoutMillis?, idleTimeoutMillis? }`                                           |
| `isGlobal`          | `boolean`                                                                                             |

## `IRedisClientOptions`

| Option                 | Type                                                | Default       |
| ---------------------- | --------------------------------------------------- | ------------- |
| `namespace`            | `string`                                            | `'default'`   |
| `host`                 | `string`                                            | `'localhost'` |
| `port`                 | `number`                                            | `6379`        |
| `db`                   | `number` — DB index                                 | `0`           |
| `path`                 | `string` — Unix socket path (alt. to host/port)     | —             |
| `password`             | `string`                                            | —             |
| `username`             | `string` — Redis 6+ ACL username                    | —             |
| `name`                 | `string` — Connection name visible in `CLIENT LIST` | —             |
| `lazyConnect`          | `boolean` — defer connect to first command          | —             |
| `enableReadyCheck`     | `boolean` — wait for 'ready' before returning       | `true`        |
| `enableOfflineQueue`   | `boolean` — buffer commands during disconnect       | `true`        |
| `connectTimeout`       | `number` (ms)                                       | `10_000`      |
| `commandTimeout`       | `number` (ms)                                       | —             |
| `keepAlive`            | `number` (ms)                                       | —             |
| `maxRetriesPerRequest` | `number`                                            | `20`          |
| `retryStrategy`        | `(times) => number \| null`                         | exponential   |
| `cluster`              | `IRedisClusterOptions`                              | —             |
| `sentinels`            | `Array<{ host, port }>`                             | —             |
| `sentinelName`         | `string`                                            | —             |
| `tls`                  | `IRedisTlsOptions`                                  | —             |

## `RedisService` — the high-level API

The injected default service for one-line ops without managing clients.

```typescript
import { RedisService, REDIS_MANAGER } from '@omnitron-dev/titan-redis';

@Service({ name: 'cart' })
class CartService {
  constructor(private readonly redis: RedisService) {}

  @Public()
  async add(userId: string, item: Item) {
    await this.redis.hset(`cart:${userId}`, item.id, JSON.stringify(item));
  }
}
```

### Connection helpers

| Method                                       | Purpose                             |
| -------------------------------------------- | ----------------------------------- |
| `getClient(namespace?)`                      | Get client by namespace             |
| `getOrThrow(namespace?)`                     | Get client or throw `NotFoundError` |
| `getOrNil(namespace?)`                       | Get client or return `null`         |
| `ping(namespace?)`                           | Health check; returns `boolean`     |
| `isReady(namespace?)`                        | Synchronous ready check             |
| `pipeline(namespace?)` / `multi(namespace?)` | Batch pipeline / transaction        |
| `createSubscriber(namespace?)`               | Dup client for pub/sub              |
| `publish(channel, message, namespace?)`      | Publish to channel                  |

### Strings, counters, expiry

```typescript
await this.redis.set('user:42:tier', 'premium', 3600);   // 3600s TTL
await this.redis.setnx('lock:job', 'taken');             // 1 if set, 0 if exists
await this.redis.incr('orders:today');
await this.redis.expire('cart:42', 600);
const ttl = await this.redis.ttl('cart:42');             // -1 no expiry, -2 not exists
```

### Hashes / sets / lists / sorted sets / streams

`RedisService` proxies every common ioredis method directly: `hget`,
`hset`, `hgetall`, `hdel`, `hexists`, `hincrby`, `hlen`, `hkeys`,
`hvals`, `hmset`, `hmget`; `sadd`, `srem`, `smembers`, `sismember`;
`lpush`, `rpush`, `lpop`, `rpop`, `lrange`, `llen`, `ltrim`; `zadd`,
`zrem`, `zrange`, `zrevrange`, `zcard`, `zscore`, `zincrby`,
`zrangebyscore`; `xadd`, `xgroup`, `xreadgroup`, `xack`, `xlen`,
`xrange`, `xread`, `xtrim`. Each accepts an optional trailing
`namespace?` argument to target a non-default client.

### Lua scripts

```typescript
const sha = await this.redis.loadScript(
  'limit',
  'return redis.call("incr", KEYS[1]) <= tonumber(ARGV[1]) and 1 or 0',
);
const allowed = await this.redis.runScript<number>('limit', ['rate:42'], [100]);
```

Scripts declared in `RedisModule.forRoot({ scripts: [...] })` are
preloaded at startup; `runScript()` re-loads automatically if Redis
evicts the SHA (NOSCRIPT error).

## `RedisManager` — low-level API

Injected via `@Inject(REDIS_MANAGER)` for cases that need the raw
ioredis client (advanced commands not in `RedisService`'s typed
surface, custom connection management, dynamic client creation).

```typescript
@Service({ name: 'analytics' })
class AnalyticsService {
  constructor(@Inject(REDIS_MANAGER) private readonly redis: RedisManager) {}

  @Public()
  async raw() {
    const client = this.redis.getClient('analytics');
    return client.call('OBJECT', 'ENCODING', 'user:42');  // arbitrary command
  }
}
```

| Method                                             | Purpose                               |
| -------------------------------------------------- | ------------------------------------- |
| `getClient(namespace?)`                            | Public `IRedisClient`                 |
| `getInternalClient(namespace?)`                    | Internal — full ioredis surface       |
| `getClients()`                                     | All registered clients                |
| `hasClient(namespace?)`                            | Existence check                       |
| `createClient(options)`                            | Dynamically create a new named client |
| `destroyClient(namespace)`                         | Tear down a client                    |
| `isHealthy(namespace?)`                            | Health check                          |
| `healthCheck()`                                    | All clients' health + latency         |
| `runScript<T>(scriptName, keys, args, namespace?)` | Execute named Lua script              |

## Decorators

### `@InjectRedis(namespace?)` — parameter

```typescript
@Service({ name: 'cart' })
class CartService {
  constructor(@InjectRedis() private readonly redis: IRedisClient) {}
}

@Service({ name: 'sessions' })
class SessionsService {
  constructor(@InjectRedis('sessions') private readonly redis: IRedisClient) {}
}
```

### `@InjectRedisManager()` — parameter

Inject the `RedisManager` directly.

### `@RedisCache(options?)` — method

```typescript
@Public()
@RedisCache({ ttl: 60, namespace: 'cache', key: (id) => `user:${id}` })
async findById(id: string) { /* … */ }
```

Options: `ttl` (seconds), `namespace`, `key` (string or function),
`keyFn` (alias), `condition` (skip if false), `refresh` (always
re-compute, bypass read).

### `@RedisLock(options?)` — method

Lightweight inline lock for simple cases. For full distributed lock
semantics use [`titan-lock`](/docs/titan/modules/lock).

```typescript
@Public()
@RedisLock({ ttl: 10, key: (id) => `invoice:${id}`, retries: 5 })
async charge(id: string) { /* … */ }
```

### `@RedisRateLimit(options)` — method

```typescript
@Public()
@RedisRateLimit({ points: 100, duration: 60_000, keyPrefix: 'rl' })
async search(q: string) { /* … */ }
```

For tier-based / sliding-window strategies use
[`titan-ratelimit`](/docs/titan/modules/ratelimit).

## Tokens

| Token                             | Purpose                                  |
| --------------------------------- | ---------------------------------------- |
| `REDIS_MANAGER`                   | The `RedisManager` instance              |
| `REDIS_MODULE_OPTIONS`            | Resolved options bundle                  |
| `REDIS_DEFAULT_NAMESPACE`         | Default namespace constant (`'default'`) |
| `getRedisClientToken(namespace?)` | Token for a named client                 |

## Lifecycle

The module's provider factory calls `RedisManager.init()` before consumers
receive the manager. It initializes non-lazy clients and preloads declared Lua
scripts. `TitanRedisModule.onStop()` calls `destroy()` after dependent modules
have stopped. Applications using the module do not need a second shutdown hook.
The manager also exposes `onModuleInit()` and `onModuleDestroy()` for hosts that
explicitly invoke those compatibility methods.

Shutdown sends `QUIT` only to a ready client and waits up to `shutdownTimeout`
for its acknowledgement. A client that is lazy, connecting or reconnecting is
disconnected immediately, so stopping an application cannot start a new Redis
connection. The deadline applies independently to clients closed in parallel.
A timeout or failed acknowledgement is logged with its namespace, then the
socket and reconnect loop are closed. This bounds shutdown; it does not prove
that an unacknowledged command reached the server.

`destroyClient(namespace)` uses the same close path and removes the registration
even when closing fails; the caller receives the failure. `destroy()` attempts
to close every owned client and logs individual failures. With
`closeClient: false`, the caller retains responsibility for connections and must
close them explicitly. Set `shutdownTimeout: 0` only when immediate disconnect
is intended; otherwise use a finite non-negative integer up to `2147483647`.

## DB-index isolation pattern

When you co-locate multiple subsystems on the same Redis instance,
isolate them by DB index per named client instead of sharing one
index. A typical layout:

| DB | Example use                  |
| -- | ---------------------------- |
| 0  | Default application data     |
| 1  | Cache (L2 for `titan-cache`) |
| 2  | Queue / messaging            |
| 3  | Sessions                     |
| 4  | Rate-limit buckets           |

Use named clients with an explicit `db` per namespace rather than
swapping `SELECT` at runtime — runtime `SELECT` is per-connection
and hard to reason about under connection-pool churn.

## Anti-patterns

- **Cross-slot ops in cluster `multi()`.** Cluster mode forbids
  multi-slot transactions. Pin keys to one slot via `{hashtag}` or
  use Lua scripts that touch one key.
- **One client for everything.** Pub/sub blocks command pipelines on
  the subscribing client. Use `createSubscriber()` for pub/sub and
  keep the default client for commands.
- **Forgetting `password` in Sentinel/cluster.** Sentinels typically
  require their own password in addition to the data-plane password.
- **Hot-loop without pipeline.** N round-trips for N small writes
  costs N×RTT. Use `pipeline()` or `multi()` for batches.
- **Using `@RedisLock` for critical multi-step work.** It's a
  convenience wrapper. For Lua-safe ownership and TTL extension, use
  [`titan-lock`](/docs/titan/modules/lock).

## See also

- [`titan-cache`](/docs/titan/modules/cache) — L2 backing via this module
- [`titan-lock`](/docs/titan/modules/lock) — robust distributed locks
- [`titan-discovery`](/docs/titan/modules/discovery), [`titan-ratelimit`](/docs/titan/modules/ratelimit),
  [`titan-notifications`](/docs/titan/modules/notifications) — all build on this module