Skip to main content
The ioredis state adapter provides persistent subscriptions and distributed locking using the ioredis package. Use this adapter when you need Redis Cluster, Sentinel, or want to reuse an existing ioredis client.

Installation

npm install @chat-adapter/state-ioredis

Basic Setup

import { Chat } from 'chat';
import { createIoRedisState } from '@chat-adapter/state-ioredis';

const chat = new Chat({
  userName: 'mybot',
  adapters: { /* ... */ },
  state: createIoRedisState({
    url: process.env.REDIS_URL,
  }),
});

Configuration

createIoRedisState(options)

Creates an ioredis state adapter instance.
export interface IoRedisStateAdapterOptions {
  /** Redis connection URL (e.g., redis://localhost:6379) */
  url: string;
  /** Key prefix for all Redis keys (default: "chat-sdk") */
  keyPrefix?: string;
  /** Logger instance for error reporting */
  logger?: Logger;
}

export interface IoRedisStateClientOptions {
  /** Existing ioredis client instance */
  client: Redis;
  /** Key prefix for all Redis keys (default: "chat-sdk") */
  keyPrefix?: string;
  /** Logger instance for error reporting */
  logger?: Logger;
}

url

Redis connection URL in format:
redis://[[username][:password]@][host][:port][/db-number]
const state = createIoRedisState({
  url: 'redis://localhost:6379',
});

client

Pass an existing ioredis client for advanced configurations:
import Redis from 'ioredis';

const client = new Redis({
  host: 'localhost',
  port: 6379,
  password: 'mypassword',
  db: 0,
  retryStrategy(times) {
    const delay = Math.min(times * 50, 2000);
    return delay;
  },
});

const state = createIoRedisState({ client });
When providing your own client, the adapter will NOT call quit() on disconnect. You’re responsible for client lifecycle management.

keyPrefix

Optional prefix for all Redis keys:
const state = createIoRedisState({
  url: process.env.REDIS_URL,
  keyPrefix: 'chatbot-prod', // default: "chat-sdk"
});

logger

Optional logger for error reporting:
import { ConsoleLogger } from 'chat';

const state = createIoRedisState({
  url: process.env.REDIS_URL,
  logger: new ConsoleLogger('debug').child('ioredis'),
});

Redis Cluster

For high-availability Redis Cluster deployments:
import Redis from 'ioredis';
import { createIoRedisState } from '@chat-adapter/state-ioredis';

const cluster = new Redis.Cluster([
  { host: 'redis-1.example.com', port: 6379 },
  { host: 'redis-2.example.com', port: 6379 },
  { host: 'redis-3.example.com', port: 6379 },
], {
  redisOptions: {
    password: process.env.REDIS_PASSWORD,
  },
});

const state = createIoRedisState({ client: cluster });

const chat = new Chat({
  userName: 'mybot',
  adapters: { /* ... */ },
  state,
});

Redis Sentinel

For Redis Sentinel (automatic failover):
import Redis from 'ioredis';
import { createIoRedisState } from '@chat-adapter/state-ioredis';

const sentinel = new Redis({
  sentinels: [
    { host: 'sentinel-1', port: 26379 },
    { host: 'sentinel-2', port: 26379 },
    { host: 'sentinel-3', port: 26379 },
  ],
  name: 'mymaster',
  password: process.env.REDIS_PASSWORD,
});

const state = createIoRedisState({ client: sentinel });

Redis Key Structure

Identical to the Redis adapter:
TypePatternExampleDescription
Subscriptions{prefix}:subscriptionschat-sdk:subscriptionsSet of subscribed thread IDs
Locks{prefix}:lock:{threadId}chat-sdk:lock:slack:C123:1234567890.123String (lock token) with TTL
Cache{prefix}:cache:{key}chat-sdk:cache:user:123:prefsJSON string with optional TTL

Inspecting Keys

# View all subscribed threads
SMEMBERS chat-sdk:subscriptions

# Check lock status
GET chat-sdk:lock:slack:C123:1234567890.123
TTL chat-sdk:lock:slack:C123:1234567890.123

# View cache
GET chat-sdk:cache:user:123:preferences

Connection Management

ioredis automatically connects on instantiation. The adapter tracks connection state internally.
const state = createIoRedisState({ url: process.env.REDIS_URL });
await state.connect(); // Waits for 'ready' event

// Use state...

await state.disconnect(); // Calls quit() if adapter owns client

Connection Events

The adapter listens for ioredis events:
import Redis from 'ioredis';

const client = new Redis(process.env.REDIS_URL);

client.on('connect', () => console.log('Connected to Redis'));
client.on('ready', () => console.log('Redis ready'));
client.on('error', (err) => console.error('Redis error:', err));
client.on('close', () => console.log('Redis connection closed'));
client.on('reconnecting', () => console.log('Reconnecting...'));

const state = createIoRedisState({ client });
ioredis automatically reconnects on network failures. Your bot will resume operation once Redis is reachable.

Thread Subscriptions

Subscriptions are stored in a Redis Set:
chat.onNewMention(async (thread, message) => {
  await thread.subscribe(); // SADD chat-sdk:subscriptions {threadId}
  await thread.post('Watching this thread!');
});

chat.onSubscribedMessage(async (thread, message) => {
  // Triggered when: SISMEMBER chat-sdk:subscriptions {threadId} == 1
  await thread.post(`Got: ${message.text}`);
});

Distributed Locking

Locks use atomic SET NX PX operations:
// Internal to Chat SDK
const lock = await state.acquireLock(threadId, 30000);
if (!lock) {
  // Another instance is processing this thread
  return;
}

try {
  await processMessage(thread, message);
} finally {
  await state.releaseLock(lock);
}

Lock Token Format

// Example lock token
"ioredis_1678901234567_a1b2c3d4e5"
//        ^ timestamp    ^ random

Extending Locks

For operations exceeding the initial TTL:
const lock = await state.acquireLock(threadId, 30000);
if (!lock) return;

try {
  const interval = setInterval(async () => {
    const extended = await state.extendLock(lock, 30000);
    if (!extended) {
      clearInterval(interval);
      throw new Error('Lost lock');
    }
  }, 10000);

  await longRunningTask();
  clearInterval(interval);
} finally {
  await state.releaseLock(lock);
}

Caching

Store JSON-serialized values with optional TTL:
const state = chat.getState();

// Store with 5-minute TTL
await state.set('rate-limit:user:123', { count: 5 }, 5 * 60 * 1000);

// Retrieve
const limit = await state.get<{ count: number }>('rate-limit:user:123');
if (limit && limit.count >= 10) {
  await thread.post('Rate limit exceeded!');
  return;
}

// Delete
await state.delete('rate-limit:user:123');

Advanced Usage

Accessing the ioredis Client

import { IoRedisStateAdapter } from '@chat-adapter/state-ioredis';

const state = createIoRedisState({ url: process.env.REDIS_URL });
await state.connect();

const client = (state as IoRedisStateAdapter).getClient();

// Use ioredis commands directly
await client.incr('custom:counter');
await client.expire('custom:counter', 3600);

const pipeline = client.pipeline();
pipeline.set('key1', 'value1');
pipeline.set('key2', 'value2');
await pipeline.exec();

Custom Retry Strategy

import Redis from 'ioredis';

const client = new Redis({
  host: 'localhost',
  port: 6379,
  retryStrategy(times) {
    const delay = Math.min(times * 50, 2000);
    return delay;
  },
  maxRetriesPerRequest: 3,
});

const state = createIoRedisState({ client });

Connection Pooling

import Redis from 'ioredis';

// Create multiple connections for high-throughput scenarios
const clients = Array.from({ length: 5 }, () => new Redis(process.env.REDIS_URL));

let clientIndex = 0;
function getClient() {
  const client = clients[clientIndex];
  clientIndex = (clientIndex + 1) % clients.length;
  return client;
}

const state = createIoRedisState({ client: getClient() });

Deployment Examples

import Redis from 'ioredis';
import { createIoRedisState } from '@chat-adapter/state-ioredis';

const cluster = new Redis.Cluster([
  { host: process.env.ELASTICACHE_ENDPOINT, port: 6379 },
], {
  dnsLookup: (address, callback) => callback(null, address),
  redisOptions: {
    tls: {},
    password: process.env.REDIS_PASSWORD,
  },
});

const chat = new Chat({
  userName: 'mybot',
  adapters: { /* ... */ },
  state: createIoRedisState({ client: cluster }),
});

Monitoring

Connection Health

import type { IoRedisStateAdapter } from '@chat-adapter/state-ioredis';

const state = createIoRedisState({ url: process.env.REDIS_URL });
const client = (state as IoRedisStateAdapter).getClient();

const isHealthy = client.status === 'ready';
console.log(`Redis status: ${client.status}`);
// Possible values: 'wait', 'connecting', 'connect', 'ready', 'close', 'end'

Metrics

// Subscription count
const count = await client.scard('chat-sdk:subscriptions');

// Active locks
const locks = await client.keys('chat-sdk:lock:*');

// Memory usage
const info = await client.info('memory');
console.log(info);

Troubleshooting

ioredis is failing to connect to Redis.Solution:
  • Check REDIS_URL is correct
  • Verify network access (security groups, firewalls)
  • Ensure Redis is running: redis-cli ping
  • Check logs for connection errors
You’re writing to a read replica in Redis Cluster.Solution: Configure cluster with scaleReads:
new Redis.Cluster([...], {
  scaleReads: 'master', // Always write to master
});
When providing your own client, the adapter doesn’t call quit() on disconnect.Solution: Manually disconnect:
await state.disconnect();
await client.quit();
Locks expire automatically via TTL, but graceful release is better.Solution: Always use try/finally:
const lock = await state.acquireLock(threadId, 30000);
if (!lock) return;
try {
  // ...
} finally {
  await state.releaseLock(lock);
}

redis vs ioredis

Feature@chat-adapter/state-redis@chat-adapter/state-ioredis
PackageOfficial redis packageCommunity ioredis package
SetupSimple connection stringSupports advanced configs
ClusterNoYes
SentinelNoYes
Client reuseNoYes
Auto-reconnectYesYes
PerformanceSimilarSimilar
Use whenSingle Redis instanceCluster, Sentinel, or existing ioredis setup
Both adapters implement the same StateAdapter interface and can be swapped without code changes.

Migration from redis

1

Install ioredis adapter

npm install @chat-adapter/state-ioredis
npm uninstall @chat-adapter/state-redis
2

Update imports

// Before
import { createRedisState } from '@chat-adapter/state-redis';

// After
import { createIoRedisState } from '@chat-adapter/state-ioredis';
3

Update configuration

// Before
const state = createRedisState({ url: process.env.REDIS_URL });

// After
const state = createIoRedisState({ url: process.env.REDIS_URL });
4

Test

Data is compatible. Keys use the same format, so existing subscriptions and cache persist.

Next Steps

Redis Adapter

Simple Redis setup for single instance

State Overview

Learn about subscriptions and locking

Deployment

Production deployment guides

ioredis Docs

Official ioredis documentation