NEW in v2.1.0 - Enterprise session management with Redis for stateful authentication and concurrent session control.
Overview
Redis provides production-grade session storage with:
Persistent Sessions - Survive server restarts
Distributed Sessions - Share across multiple instances
Sliding Expiration - Extend sessions on user activity
Concurrent Limits - Control max sessions per user
Instant Revocation - Immediate logout across all devices
Session Analytics - Track active users and session patterns
Why Redis Sessions?
Token-based (Stateless) Best for : Serverless, Cloud Run, stateless architectures
No server storage required
Lower infrastructure costs
Simpler deployment
Cannot revoke individual tokens
Session-based (Stateful) Best for : Enterprise apps, multi-instance deployments
Longer session lifetimes (24+ hours)
Instant revocation
Concurrent session limits
User session tracking
Quick Start
Deploy Redis
Docker Compose
Kubernetes
Cloud Managed
# docker-compose.yml
redis-session :
image : redis:7-alpine
command : >
redis-server
--requirepass ${REDIS_PASSWORD}
--maxmemory 256mb
--maxmemory-policy allkeys-lru
ports :
- "6379:6379"
volumes :
- redis-session-data:/data
healthcheck :
test : [ "CMD" , "redis-cli" , "ping" ]
interval : 10s
timeout : 3s
retries : 3
volumes :
redis-session-data :
# Start Redis
export REDIS_PASSWORD = $( openssl rand -base64 32 )
docker compose up -d redis-session
# Test connection
docker compose exec redis-session redis-cli -a $REDIS_PASSWORD ping
# Expected: PONG
Configure Application
# .env or Kubernetes ConfigMap
AUTH_MODE = session
SESSION_BACKEND = redis
# Redis connection
REDIS_URL = redis://redis-session:6379/0
REDIS_PASSWORD = your-redis-password-here
REDIS_SSL = false # true for production
# Session settings
SESSION_TTL_SECONDS = 86400 # 24 hours
SESSION_SLIDING_WINDOW = true # Extend on activity
SESSION_MAX_CONCURRENT = 5 # Per user
SESSION_REFRESH_THRESHOLD = 3600 # Refresh if < 1hr left
Always use SSL/TLS in production : Set REDIS_SSL=true and use rediss:// URL scheme.
Test Session Store
from mcp_server_langgraph.auth.session import SessionManager
# Initialize session manager
session_mgr = SessionManager()
# Create session
session = await session_mgr.create_session(
user_id = "alice" ,
metadata = { "ip" : "192.168.1.1" , "user_agent" : "..." }
)
print ( f "Session ID: { session.session_id } " )
print ( f "Expires: { session.expires_at } " )
# Get session
retrieved = await session_mgr.get_session(session.session_id)
print ( f "User: { retrieved.user_id } " )
print ( f "Active: { retrieved.is_active } " )
# Refresh session (extends TTL)
refreshed = await session_mgr.refresh_session(session.session_id)
print ( f "New expiry: { refreshed.expires_at } " )
# Revoke session
await session_mgr.revoke_session(session.session_id)
Verify in Redis
# Connect to Redis
docker compose exec redis-session redis-cli -a $REDIS_PASSWORD
# List all sessions
KEYS session: *
# Get session details
GET session:abc123...
# Check session TTL
TTL session:abc123...
Session Lifecycle
Session Configuration
Session TTL
## Fixed TTL (no extension)
SESSION_TTL_SECONDS =3600 # 1 hour
SESSION_SLIDING_WINDOW =false
## Sliding window (extends on activity)
SESSION_TTL_SECONDS =86400 # 24 hours
SESSION_SLIDING_WINDOW =true
SESSION_REFRESH_THRESHOLD =3600 # Refresh if < 1hr left
Fixed TTL : Session expires exactly after TTL, regardless of activity.
Sliding Window : Session extends on each request, up to max TTL.
Concurrent Sessions
## Limit concurrent sessions per user
SESSION_MAX_CONCURRENT =5
## Behavior when limit exceeded:
## - Oldest session is automatically revoked
## - User notified of revoked session
Use cases:
1 session : Single device login (revokes other devices)
3-5 sessions : Normal multi-device usage
Unlimited : Set to 0 or omit
Store additional context with sessions:
session = await session_mgr.create_session(
user_id = "alice" ,
metadata = {
"ip_address" : "192.168.1.1" ,
"user_agent" : "Mozilla/5.0..." ,
"device_id" : "mobile-abc123" ,
"location" : "San Francisco, CA" ,
"created_at" : "2025-10-12T10:00:00Z"
}
)
Access metadata:
session = await session_mgr.get_session(session_id)
print ( f "Device: { session.metadata[ 'device_id' ] } " )
print ( f "Location: { session.metadata[ 'location' ] } " )
API Operations
Create Session
from mcp_server_langgraph.auth.session import SessionManager
session_mgr = SessionManager()
## Simple session
session = await session_mgr.create_session( user_id = "alice" )
## With metadata
session = await session_mgr.create_session(
user_id = "alice" ,
metadata = { "ip" : "192.168.1.1" , "device" : "mobile" }
)
## Custom TTL
session = await session_mgr.create_session(
user_id = "alice" ,
ttl_seconds = 7200 # 2 hours (overrides default)
)
Get Session
## Get by session ID
session = await session_mgr.get_session(session_id)
if session and session.is_active :
print(f"User : { session.user_id } ")
else:
print("Session expired or invalid")
## Get all user sessions
sessions = await session_mgr.get_user_sessions(user_id="alice")
for s in sessions :
print(f"{s.session_id} : { s.created_at } ")
Refresh Session
## Manual refresh
session = await session_mgr.refresh_session ( session_id )
print ( f "Extended until: {session.expires_at}" )
## Automatic refresh (if sliding window enabled)
## Happens automatically on get_session() calls
Revoke Session
## Revoke single session
await session_mgr.revoke_session ( session_id )
## Revoke all user sessions (logout from all devices)
await session_mgr.revoke_user_sessions ( user_id = "alice" )
## Revoke with reason
await session_mgr.revoke_session (
session_id,
reason = "Password changed"
)
Session Analytics
## Count active sessions
total = await session_mgr.count_active_sessions ()
print ( f "Active sessions: {total}" )
## Count user sessions
user_count = await session_mgr.count_user_sessions ( "alice" )
print ( f "Alice has {user_count} active sessions" )
## List all active users
users = await session_mgr.list_active_users ()
print ( f "Active users: {users}" )
Integration with Authentication
Keycloak + Redis Sessions
from mcp_server_langgraph.auth.keycloak import KeycloakClient
from mcp_server_langgraph.auth.session import SessionManager
keycloak = KeycloakClient()
session_mgr = SessionManager()
## Login flow
async def login ( username : str , password : str ):
# Authenticate with Keycloak
tokens = await keycloak.authenticate(username, password)
# Create session
session = await session_mgr.create_session(
user_id = username,
metadata = {
"access_token" : tokens[ "access_token" ],
"refresh_token" : tokens[ "refresh_token" ],
"expires_in" : tokens[ "expires_in" ]
}
)
return session.session_id
## API request flow
async def authenticate_request ( session_id : str ):
# Get session
session = await session_mgr.get_session(session_id)
if not session or not session.is_active:
raise Unauthorized( "Session expired" )
# Verify access token with Keycloak
user_info = await keycloak.get_user_info(
session.metadata[ "access_token" ]
)
# Refresh session TTL (if sliding)
await session_mgr.refresh_session(session_id)
return user_info
## Logout flow
async def logout ( session_id : str ):
session = await session_mgr.get_session(session_id)
# Revoke Keycloak tokens
await keycloak.logout(session.metadata[ "refresh_token" ])
# Delete session
await session_mgr.revoke_session(session_id)
Production Deployment
High Availability
Deploy Redis with replication:
## Kubernetes with Helm
helm install redis-session bitnami/redis \
--set architecture=replication \
--set master.persistence.enabled= true \
--set master.persistence.size=20Gi \
--set replica.replicaCount= 2 \
--set replica.persistence.enabled= true \
--set sentinel.enabled= true
## Application connects to sentinel
REDIS_URL = redis-sentinel://redis-session:26379/mymaster
``` python
Benefits:
- ** Automatic failover ** - Sentinel promotes replica on master failure
- ** Zero downtime ** - Sessions preserved during failover
- ** Read scaling ** - Read from replicas
#### Persistence
Configure Redis persistence:
redis.conf
save 900 1 # Save after 900s if >= 1 key changed
save 300 10 # Save after 300s if >= 10 keys changed
save 60 10000 # Save after 60s if >= 10000 keys changed
AOF (append-only file) for durability
appendonly yes
appendfsync everysec
Or use managed Redis with automatic backups (Memorystore, ElastiCache, Azure Cache).
#### Security
**Authentication** :
Always use password
REDIS_PASSWORD=strong-random-password
requirepass strong-random-password
TLS/SSL in production
REDIS_SSL=true
REDIS_URL=rediss://redis-session:6380/0
Redis 6+ with ACL
REDIS_USERNAME=sessions
REDIS_PASSWORD=password
**Network isolation** :
- Private VPC/subnet
- Firewall rules (allow only app servers)
- No public IP exposure
#### Memory Management
redis.conf
maxmemory 2gb
maxmemory-policy allkeys-lru # Evict least recently used
Or volatile-lru (evict only keys with TTL)
maxmemory-policy volatile-lru
Monitor memory usage:
```bash
redis-cli INFO memory
Monitoring
Key metrics to track :
## Active sessions
redis_db_keys{ db =" db 0"}
## Memory usage
redis_memory_used_bytes / redis_memory_max_bytes
## Commands per second
rate(redis_commands_processed_total[1 m ])
## Hit ratio
rate(redis_keyspace_hits_total[5 m ]) /
(rate(redis_keyspace_hits_total[5 m ]) + rate(redis_keyspace_misses_total[5 m ]))
## Evicted keys (should be low)
rate(redis_evicted_keys_total[5 m ])
Alerts :
Memory usage > 80%
Evicted keys > 0
Hit ratio < 90%
Replication lag > 5s
Troubleshooting
# Check Redis status
docker compose ps redis-session
kubectl get pods -l app.kubernetes.io/name=redis
# Test connectivity
redis-cli -h redis-session -p 6379 -a $REDIS_PASSWORD ping
# Check logs
docker compose logs redis-session
kubectl logs -l app.kubernetes.io/name=redis
Error : NOAUTH Authentication requiredSolutions :
Check REDIS_PASSWORD environment variable
Verify redis.conf has requirepass set
Use -a password flag with redis-cli
Possible causes :
Session expired (check TTL)
Redis evicted key (memory full)
Wrong Redis database (check /0 in URL)
Session was revoked
Debug :# Check if session exists in Redis
import redis
r = redis.from_url( REDIS_URL , password = REDIS_PASSWORD )
exists = r.exists( f "session: { session_id } " )
ttl = r.ttl( f "session: { session_id } " )
print ( f "Exists: { exists } , TTL: { ttl } " )
Solutions :
Reduce TTL : Lower SESSION_TTL_SECONDS
Enable eviction : Set maxmemory-policy allkeys-lru
Increase memory : Scale up Redis instance
Clean expired : Redis auto-expires, but run FLUSHDB if needed
Check current usage :redis-cli INFO memory | grep used_memory_human
redis-cli DBSIZE
Performance tuning :
Use connection pooling (automatic in Python redis client)
Enable pipelining for batch operations
Use local Redis instance (reduce network latency)
Check Redis logs for slow queries
# Enable slow log
CONFIG SET slowlog-log-slower-than 10000 # 10ms
SLOWLOG GET 10
Migration from Token-based
Switching from stateless tokens to Redis sessions:
Deploy Redis
Deploy Redis instance (see Quick Start above)
Update Configuration
# Change auth mode
AUTH_MODE = session # was: token
SESSION_BACKEND = redis
# Add Redis config
REDIS_URL = redis://redis-session:6379/0
REDIS_PASSWORD = ...
Dual Mode Support (Optional)
Support both tokens and sessions during migration: # Check Authorization header
auth_header = request.headers.get( "Authorization" )
cookie = request.cookies.get( "session_id" )
if auth_header:
# Token-based auth (legacy)
token = auth_header.replace( "Bearer " , "" )
user = await auth.verify_token(token)
elif cookie:
# Session-based auth (new)
session = await session_mgr.get_session(cookie)
user = session.user_id
else :
raise Unauthorized()
Update Client Applications
Migrate clients from token to session: Before (Token):const token = localStorage . getItem ( 'access_token' );
fetch ( '/api/message' , {
headers: { 'Authorization' : `Bearer ${ token } ` }
});
After (Session):// Session cookie sent automatically
fetch ( '/api/message' , {
credentials: 'include' // Send cookies
});
Deprecate Token Support
After migration period, remove token support: # Final config
AUTH_MODE = session
# Remove: JWT_SECRET_KEY, JWT_EXPIRATION_SECONDS
Next Steps
Production Ready : Redis sessions provide enterprise-grade session management with instant revocation and multi-device support!