The Redis state adapter provides persistent subscriptions and distributed locking using the official redis package. Recommended for production deployments.
Installation
npm install @chat-adapter/state-redis
Basic Setup
import { Chat } from 'chat' ;
import { createRedisState } from '@chat-adapter/state-redis' ;
const chat = new Chat ({
userName: 'mybot' ,
adapters: { /* ... */ },
state: createRedisState ({
url: process . env . REDIS_URL ,
}),
});
Configuration
createRedisState(options)
Creates a Redis state adapter instance.
export interface RedisStateAdapterOptions {
/** 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 ;
}
url
Required. Redis connection URL in format:
redis://[[username][:password]@][host][:port][/db-number]
Local Development
Production with Auth
TLS (Vercel, Railway)
const state = createRedisState ({
url: 'redis://localhost:6379' ,
});
keyPrefix
Optional prefix for all Redis keys. Useful for:
Sharing Redis instance across multiple bots
Separating staging/production environments
Namespacing in multi-tenant scenarios
const state = createRedisState ({
url: process . env . REDIS_URL ,
keyPrefix: 'mybot-prod' , // default: "chat-sdk"
});
// Keys stored as:
// mybot-prod:subscriptions
// mybot-prod:lock:slack:C123:1234567890.123
// mybot-prod:cache:user:123:preferences
logger
Optional logger for error reporting. Defaults to console logger.
import { ConsoleLogger } from 'chat' ;
const state = createRedisState ({
url: process . env . REDIS_URL ,
logger: new ConsoleLogger ( 'debug' ). child ( 'redis' ),
});
Environment Variables
REDIS_URL
The createRedisState() helper automatically reads REDIS_URL if not provided:
// These are equivalent:
const state = createRedisState ({ url: process . env . REDIS_URL });
const state = createRedisState (); // Auto-reads REDIS_URL
If REDIS_URL is not set and no url is provided, createRedisState() throws an error.
Redis Key Structure
The adapter uses the following key patterns:
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
Example Redis Commands
# View all subscribed threads
SMEMBERS chat-sdk:subscriptions
# Check if thread is locked
GET chat-sdk:lock:slack:C123:1234567890.123
# View cached value
GET chat-sdk:cache:user:123:preferences
TTL chat-sdk:cache:user:123:preferences
# View all keys
KEYS chat-sdk: *
Connection Management
The adapter automatically connects when the Chat instance initializes. You typically don’t need to manage connections manually.
Manual Connection
const state = createRedisState ({ url: process . env . REDIS_URL });
await state . connect ();
// Use state...
await state . disconnect ();
Connection Errors
Connection errors are logged but don’t crash your application:
// Logs: "Redis client error: { error: ... }"
state . on ( 'error' , ( err ) => {
console . error ( 'Redis error:' , err );
});
The redis package automatically reconnects on network failures. Your bot will resume normal operation once Redis is reachable.
Thread Subscriptions
Subscriptions are stored in a Redis Set for efficient membership checks.
chat . onNewMention ( async ( thread , message ) => {
await thread . subscribe (); // SADD chat-sdk:subscriptions {threadId}
});
chat . onSubscribedMessage ( async ( thread , message ) => {
// Checked via: SISMEMBER chat-sdk:subscriptions {threadId}
await thread . post ( `Message received: ${ message . text } ` );
});
// Unsubscribe
await thread . unsubscribe (); // SREM chat-sdk:subscriptions {threadId}
Checking Subscription Status
const isSubscribed = await thread . isSubscribed ();
if ( isSubscribed ) {
await thread . post ( 'Still monitoring this thread!' );
}
Distributed Locking
Locks prevent concurrent message processing across serverless instances or webhook retries.
How Locks Work
// Automatic locking (internal to Chat SDK)
const lock = await state . acquireLock ( threadId , 30000 ); // 30s TTL
if ( ! lock ) {
// Already locked by another instance
return ;
}
try {
// Process message...
} finally {
await state . releaseLock ( lock );
}
Lock Implementation
Locks use Redis SET NX PX for atomic acquisition:
SET chat-sdk:lock:{threadId} {token} NX PX {ttlMs}
Properties:
NX - Only set if key doesn’t exist (atomic check-and-set)
PX - Expire after TTL milliseconds (auto-cleanup)
Token - Unique token ensures only lock holder can release
Extending Locks
For long-running operations:
const lock = await state . acquireLock ( threadId , 30000 );
if ( ! lock ) return ;
try {
// Start long operation...
// Refresh TTL every 10 seconds
const interval = setInterval ( async () => {
const extended = await state . extendLock ( lock , 30000 );
if ( ! extended ) {
clearInterval ( interval );
throw new Error ( 'Lost lock ownership' );
}
}, 10000 );
await performLongTask ();
clearInterval ( interval );
} finally {
await state . releaseLock ( lock );
}
Locks auto-expire after TTL. Always set a TTL longer than your expected processing time, and use extendLock() for operations that might exceed it.
Caching
The cache API stores JSON-serialized values with optional TTL.
const state = chat . getState ();
// Store with 10-minute TTL
await state . set ( 'user:123:preferences' , {
theme: 'dark' ,
notifications: true ,
}, 10 * 60 * 1000 );
// Retrieve
const prefs = await state . get < UserPreferences >( 'user:123:preferences' );
if ( prefs ) {
console . log ( `Theme: ${ prefs . theme } ` );
}
// Store without TTL (persist forever)
await state . set ( 'config:version' , '1.2.3' );
// Delete
await state . delete ( 'user:123:preferences' );
Serialization
Values are automatically JSON-serialized:
// Objects
await state . set ( 'key' , { foo: 'bar' });
const obj = await state . get <{ foo : string }>( 'key' ); // { foo: 'bar' }
// Arrays
await state . set ( 'key' , [ 1 , 2 , 3 ]);
const arr = await state . get < number []>( 'key' ); // [1, 2, 3]
// Primitives
await state . set ( 'key' , 'hello' );
const str = await state . get < string >( 'key' ); // 'hello'
Non-JSON values (functions, symbols) will be lost during serialization. Use plain objects and primitives.
Advanced Usage
Accessing the Redis Client
For advanced Redis operations:
import { RedisStateAdapter } from '@chat-adapter/state-redis' ;
const state = createRedisState ({ url: process . env . REDIS_URL });
await state . connect ();
const client = ( state as RedisStateAdapter ). getClient ();
// Use any redis command
await client . incr ( 'custom:counter' );
const count = await client . get ( 'custom:counter' );
Custom Key Prefix
Separate environments using different prefixes:
const prodState = createRedisState ({
url: process . env . REDIS_URL ,
keyPrefix: 'mybot-prod' ,
});
const stagingState = createRedisState ({
url: process . env . REDIS_URL ,
keyPrefix: 'mybot-staging' ,
});
Deployment Examples
Vercel
Railway
Docker Compose
// Deploy with Vercel Redis (KV)
export default async function handler ( req : Request ) {
const chat = new Chat ({
userName: 'mybot' ,
adapters: { /* ... */ },
state: createRedisState (), // Reads REDIS_URL from env
});
return chat . webhooks . slack ( req );
}
Monitoring
Key Metrics to Track
// Subscription count
const count = await client . sCard ( 'chat-sdk:subscriptions' );
console . log ( `Active subscriptions: ${ count } ` );
// Active locks
const locks = await client . keys ( 'chat-sdk:lock:*' );
console . log ( `Active locks: ${ locks . length } ` );
// Cache size
const cached = await client . keys ( 'chat-sdk:cache:*' );
console . log ( `Cached keys: ${ cached . length } ` );
Health Check
import { RedisStateAdapter } from '@chat-adapter/state-redis' ;
export async function healthCheck () : Promise < boolean > {
const state = createRedisState ({ url: process . env . REDIS_URL });
try {
await state . connect ();
await state . set ( 'health' , Date . now (), 5000 ); // 5s TTL
const value = await state . get ( 'health' );
await state . disconnect ();
return value !== null ;
} catch {
return false ;
}
}
Troubleshooting
Error: Redis url is required
The REDIS_URL environment variable is not set. Solution: export REDIS_URL = redis :// localhost : 6379
Or provide it explicitly: createRedisState ({ url: 'redis://localhost:6379' })
Error: RedisStateAdapter is not connected
You’re calling state methods before connect() is called. Solution:
The Chat SDK calls connect() automatically. If using the adapter directly:await state . connect ();
await state . subscribe ( threadId );
Locks are expiring too quickly
Your message handlers are taking longer than the default 30s lock TTL. Solution:
Use extendLock() to refresh the TTL during long operations, or increase the initial TTL in your custom locking logic.
Subscriptions lost after restart
This is normal with MemoryStateAdapter. With RedisStateAdapter, subscriptions persist. Solution:
Verify you’re using @chat-adapter/state-redis (not @chat-adapter/state-memory):import { createRedisState } from '@chat-adapter/state-redis' ;
Migration from Memory Adapter
Install Redis adapter
npm install @chat-adapter/state-redis
Set up Redis
Use a hosted Redis provider (Vercel KV, Upstash, Railway) or run locally: docker run -d -p 6379:6379 redis:7-alpine
Update configuration
// Before
import { createMemoryState } from '@chat-adapter/state-memory' ;
const state = createMemoryState ();
// After
import { createRedisState } from '@chat-adapter/state-redis' ;
const state = createRedisState ({ url: process . env . REDIS_URL });
Set environment variable
export REDIS_URL = redis :// localhost : 6379
Re-subscribe threads
Existing subscriptions from memory are lost. Users will need to @-mention your bot again to re-subscribe.
Next Steps
ioredis Adapter Use ioredis for Cluster or Sentinel
State Overview Learn about subscriptions and locking
Thread API Explore thread.subscribe() and state methods
Deployment Production deployment guides