Skip to main content
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]
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:
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

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

// 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

The REDIS_URL environment variable is not set.Solution:
export REDIS_URL=redis://localhost:6379
Or provide it explicitly:
createRedisState({ url: 'redis://localhost:6379' })
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);
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.
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

1

Install Redis adapter

npm install @chat-adapter/state-redis
2

Set up Redis

Use a hosted Redis provider (Vercel KV, Upstash, Railway) or run locally:
docker run -d -p 6379:6379 redis:7-alpine
3

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 });
4

Set environment variable

export REDIS_URL=redis://localhost:6379
5

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

Build docs developers (and LLMs) love