Implementing Redis-Based Session State Sharing
The initial version of Claude Control stored session state in memory. Every server restart wiped all session data. This approach had obvious limitations in a multi-pod environment, so we introduced Redis for shared session state.
Designing the Redis Session Store
A session's state consists of multiple fields, making a Hash structure the natural fit.
import redis.asyncio as redis
import json
from datetime import datetime
class RedisSessionStore:
def __init__(self, redis_url: str = "redis://localhost:6379"):
self.redis = redis.from_url(redis_url, decode_responses=True)
self.prefix = "claude:session:"
async def save_session(self, session_id: str, data: dict):
key = f"{self.prefix}{session_id}"
data["updated_at"] = datetime.now().isoformat()
await self.redis.hset(key, mapping={
k: json.dumps(v) if isinstance(v, (dict, list)) else str(v)
for k, v in data.items()
})
await self.redis.expire(key, 86400) # 24-hour TTL
async def get_session(self, session_id: str) -> dict | None:
key = f"{self.prefix}{session_id}"
data = await self.redis.hgetall(key)
if not data:
return None
return {k: self._parse_value(v) for k, v in data.items()}
def _parse_value(self, value: str):
try:
return json.loads(value)
except (json.JSONDecodeError, TypeError):
return value
Session Data Caching Strategy
Input, output, and status for every session are cached in Redis. The key schema is as follows:
# Session base info
claude:session:{session_id} # Hash: status, workdir, created_at
# Session I/O logs
claude:session:{session_id}:logs # List: up to 100 most recent log entries
# Session metrics
claude:session:{session_id}:metrics # Hash: prompt_count, token_usage
# Active session registry
claude:sessions:active # Set: IDs of currently active sessions
Pub/Sub is used to propagate session state changes to other instances in real time.
async def publish_session_event(self, session_id: str, event: str, data: dict):
channel = "claude:events"
message = json.dumps({
"session_id": session_id,
"event": event,
"data": data,
"timestamp": datetime.now().isoformat(),
})
await self.redis.publish(channel, message)
async def subscribe_events(self):
pubsub = self.redis.pubsub()
await pubsub.subscribe("claude:events")
async for message in pubsub.listen():
if message["type"] == "message":
event = json.loads(message["data"])
await self._handle_event(event)
Pain Points
Connection pool management turned into a lengthy debugging session. redis.asyncio was exhausting the connection pool, which turned out to be caused by code that wasn't returning connections promptly. Tuning max_connections and tracking down those code paths fixed it. There was also a serialization issue where datetime objects weren't handled during JSON encoding — a custom encoder took care of that.
After introducing Redis, sessions survive server restarts and multiple pods can reference the same session state consistently. This foundation becomes central to the Kubernetes multi-pod support built later.