Session Storage
Why Redis for Sessions?
HTTP is stateless — sessions track user identity across requests. Storing sessions in Redis provides:
- Sub-millisecond read/write latency
- Shared state across all app servers — no sticky sessions required
- Automatic expiry via TTL
- Horizontal scaling of web tier independent of session storage
Browser → Load Balancer → App Server 1 ─┐
→ App Server 2 ─┼→ Redis (sessions)
→ App Server 3 ─┘
Without shared session storage, load balancers must pin users to specific servers (sticky sessions), complicating deploys and failover.
Session Key Pattern
SETEX session:abc123def 1800 '{"user_id":1001,"role":"admin"}'
GET session:abc123def
DEL session:abc123def
TTL session:abc123def
Use cryptographically random session IDs — never sequential or predictable values.
import secrets
session_id = secrets.token_urlsafe(32) # 256 bits of entropy
Hash-Based Sessions
Hashes allow field-level updates without rewriting the entire session blob.
HSET session:abc123 user_id 1001 role admin last_active 1718200000
EXPIRE session:abc123 1800
HGETALL session:abc123
HSET session:abc123 last_active 1718203600
HINCRBY session:abc123 request_count 1
Prefer hashes when sessions have multiple fields updated independently.
Session Lifecycle
import secrets
import time
import redis
r = redis.Redis(decode_responses=True)
SESSION_TTL = 1800 # 30 minutes
def create_session(user_id, role):
session_id = secrets.token_urlsafe(32)
key = f"session:{session_id}"
r.hset(key, mapping={
"user_id": user_id,
"role": role,
"created_at": int(time.time()),
})
r.expire(key, SESSION_TTL)
return session_id
def get_session(session_id):
if not session_id:
return None
return r.hgetall(f"session:{session_id}")
def destroy_session(session_id):
r.delete(f"session:{session_id}")
Sliding Expiration
Refresh TTL on each request to keep active users logged in while expiring idle sessions.
def touch_session(session_id):
key = f"session:{session_id}"
if r.exists(key):
r.expire(key, SESSION_TTL)
r.hset(key, "last_active", int(time.time()))
Call touch_session on every authenticated request.
Session Security
Regenerate Session ID on Login
Prevents session fixation attacks:
def regenerate_session(old_id, user_id, role):
old_data = r.hgetall(f"session:{old_id}")
new_id = secrets.token_urlsafe(32)
new_key = f"session:{new_id}"
r.hset(new_key, mapping=old_data or {"user_id": user_id, "role": role})
r.expire(new_key, SESSION_TTL)
r.delete(f"session:{old_id}")
return new_id
Bind Session to Client Fingerprint (Optional)
def validate_session(session_id, request):
session = r.hgetall(f"session:{session_id}")
if not session:
return None
if session.get("user_agent") != request.headers.get("User-Agent"):
destroy_session(session_id)
return None
return session
Trade-off: breaks legitimate UA changes (browser updates). Use carefully.
Secure Cookie Settings
Set-Cookie: session_id=...; HttpOnly; Secure; SameSite=Strict; Path=/
Never expose session IDs in URLs.
Session Payload Guidelines
Store IDs and flags, not full objects:
# Good
{"user_id": 1001, "role": "admin", "permissions_version": 3}
# Bad — stale data, large memory footprint
{"user_id": 1001, "profile": {...500KB of data...}}
Fetch fresh user data from DB or cache on each request using the session’s user_id.
Framework Integration
| Framework | Redis Session Backend |
|---|---|
| Express (Node.js) | connect-redis |
| Django | django-redis sessions |
| Rails | redis-session-store gem |
| Spring Boot | Spring Session Data Redis |
| Laravel | redis session driver in .env |
// Express example
const session = require('express-session');
const RedisStore = require('connect-redis').default;
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { secure: true, httpOnly: true, maxAge: 1800000 }
}));
Scaling Considerations
- Use Redis Sentinel or Cluster for HA — session loss forces mass re-login
- Set reasonable TTL: 15–30 minutes for sensitive apps, longer for low-risk
- Monitor memory:
INFO memory, count keys matchingsession:* - Consider separate Redis instance for sessions vs cache (isolation)
- Plan for session migration during Redis upgrades (dual-write briefly)
# Monitor session key count
redis-cli --scan --pattern 'session:*' | wc -l
INFO memory
Best Practices
- Use cryptographically random session IDs (≥128 bits)
- Regenerate session ID on privilege elevation (login)
- Store minimal data in session — IDs not objects
- Enable sliding expiration for better UX
- Run Redis with persistence (AOF) if session loss is unacceptable
Common Mistakes
| Mistake | Impact |
|---|---|
| Predictable session IDs | Session hijacking |
| No TTL on sessions | Memory leak, forever sessions |
| Storing passwords in session | Security breach if Redis compromised |
| Same Redis for cache and sessions | FLUSHDB on cache wipes all sessions |
| Sticky sessions instead of Redis | Poor failover, uneven load |
Troubleshooting
Users logged out unexpectedly:
INFO persistence
# AOF/RDB failure + restart = empty Redis
CONFIG GET maxmemory-policy
# allkeys-lru evicting sessions — use volatile-lru or separate instance
Session not found intermittently:
# Multiple Redis instances? App hitting different nodes without replication?
INFO replication
Memory growing:
redis-cli --scan --pattern 'session:*' | wc -l
# Orphan sessions — ensure destroy_session on logout
Performance Tips
- Use pipelining when reading session + related cache keys
- Hash encoding is efficient for typical session sizes (< 1 KB)
- Connection pool size ≈ app server thread count
- Avoid serializing large objects — keep sessions under 2 KB
Production Scenario
A banking portal migrated from server-side file sessions to Redis with Sentinel HA. Three web tiers behind an ALB shared 2M daily sessions in a dedicated 8 GB Redis instance. Sliding 20-minute TTL with touch on each request. Session regeneration on login blocked fixation attacks in penetration testing. Separate ACL user (session-app) restricted to session:* keys only. Failover test: Sentinel promoted replica in 8 seconds; 0.02% of users required re-login (sessions on failed primary only).
Redis sessions eliminate server affinity and simplify horizontal scaling — design security and HA before handling your first million sessions.