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
URL Connection
Existing Client
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]
Simple Connection
With Authentication
TLS Connection
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:
Type Pattern Example Description 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 );
}
// 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
AWS ElastiCache Cluster
Google Cloud Memorystore
Azure Cache for Redis
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
READONLY error in Cluster mode
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-ioredisPackage Official redis package Community ioredis package Setup Simple connection string Supports advanced configs Cluster No Yes Sentinel No Yes Client reuse No Yes Auto-reconnect Yes Yes Performance Similar Similar Use when Single Redis instance Cluster, Sentinel, or existing ioredis setup
Both adapters implement the same StateAdapter interface and can be swapped without code changes.
Migration from redis
Install ioredis adapter
npm install @chat-adapter/state-ioredis
npm uninstall @chat-adapter/state-redis
Update imports
// Before
import { createRedisState } from '@chat-adapter/state-redis' ;
// After
import { createIoRedisState } from '@chat-adapter/state-ioredis' ;
Update configuration
// Before
const state = createRedisState ({ url: process . env . REDIS_URL });
// After
const state = createIoRedisState ({ url: process . env . REDIS_URL });
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