Session Management Production Tips
Choosing and deploying a session strategy is only half the battle. The other half is solving the problems that arise in production. This page covers the session strategy selection matrix, safe session handling during zero-downtime deployment, and session-related incident response.
Session Strategy Selection Matrixβ
| Condition | Sticky Session | Tomcat Cluster | Redis Session |
|---|---|---|---|
| 2 or fewer servers | Best | Possible | Over-engineering |
| 3~5 servers | Possible | Best | Recommended |
| 5+ servers | Not recommended | Not recommended | Required |
| Cloud/Kubernetes | Not recommended | Difficult | Required |
| Legacy app (no code changes) | Best | Possible | Difficult |
| HA is top priority | Not recommended | Possible | Best |
| Large session data (1KB+) | Possible | Not recommended | Best |
| Zero-downtime deployment | Caution | Caution | Best |
Conclusion: Start with Redis for new projects. For existing systems, migrate gradually based on scale.
Safe Session Handling During Zero-Downtime Deploymentβ
Follow this sequence to prevent session loss during deployments.
Rolling Update Procedureβ
#!/bin/bash
# rolling-deploy.sh
SERVERS=("10.0.0.1" "10.0.0.2" "10.0.0.3")
LB_API="http://lb.internal/api"
NEW_VERSION=$1
for server in "${SERVERS[@]}"; do
echo "=== Deploying to $server ==="
# 1. Remove server from load balancer
curl -X PUT "$LB_API/servers/$server/status" -d '{"status": "draining"}'
# 2. Wait for existing connections to complete
sleep 30
# 3. Deploy new version
ssh "$server" "sudo systemctl stop tomcat && \
cp /releases/$NEW_VERSION.war /opt/tomcat/webapps/ROOT.war && \
sudo systemctl start tomcat"
# 4. Wait for health check to pass
until curl -s "http://$server:8080/health" | grep -q '"status":"UP"'; do
echo "Waiting for $server to become healthy..."
sleep 5
done
# 5. Return server to load balancer
curl -X PUT "$LB_API/servers/$server/status" -d '{"status": "active"}'
# 6. Stabilization wait before moving to next server
sleep 15
done
echo "Rolling deploy complete!"
Sticky Session + Zero-Downtime Deploymentβ
# Before taking down a server with Sticky Session:
# Set server state to DRAINING in Apache balancer-manager
curl "http://localhost/balancer-manager?cmd=update&w=loadbalancer&sw=server1&ws=d"
# d=Draining: continues handling existing connections, no new connections
# Wait for sessions to naturally expire
sleep 1800 # 30-minute session timeout (monitor and judge in practice)
sudo systemctl stop tomcat1
Session Incident Responseβ
Incident 1: Users Keep Getting Logged Outβ
Symptom: Users redirected to login page shortly after logging in
Possible causes:
1. Sticky Session not configured (requests distributed across servers)
2. Session timeout is too short
3. Redis connection failure causing session lookup to fail
4. jvmRoute mismatch between load balancer and Tomcat
Debugging:
# Check session IDs and servers in logs
grep "JSESSIONID" /var/log/apache2/access.log | tail -20
# If JSESSIONID=ABC.server1 changes to JSESSIONID=ABC.server2 β Sticky Session issue
# Check Redis connection
redis-cli -h redis-host ping
redis-cli -h redis-host KEYS "session:*" | wc -l
# Check Tomcat logs for session-related errors
grep -i "session\|serializ\|redis" /opt/tomcat/logs/catalina.out | tail -50
Incident 2: Redis Failure β All Sessions Lostβ
Symptom: All users logged out after Redis server goes down
Response:
1. Use Redis Sentinel or Cluster for redundancy (prevention)
2. Show users a re-login message when incident occurs
3. Store critical action data in DB as well (in-progress orders, payments, etc.)
Graceful handling of Redis failure in Spring Session:
@Configuration
public class SessionConfig {
@Bean
public HttpSessionIdResolver sessionIdResolver() {
// Cookie-based session ID survives Redis failure
return CookieHttpSessionIdResolver.withCookieSerializer(
new DefaultCookieSerializer()
);
}
}
Incident 3: Session Explosion (Memory Leak)β
Symptom: Redis memory rapidly increasing, OOM errors
Cause: Missing session expiry configuration, large objects stored in sessions
# Check Redis memory usage
redis-cli INFO memory | grep used_memory_human
# Count sessions
redis-cli KEYS "session:*" | wc -l
# Find sessions with no TTL (missing expiry)
redis-cli --scan --pattern "session:*" | while read key; do
ttl=$(redis-cli TTL "$key")
if [ "$ttl" -eq -1 ]; then
echo "No TTL: $key"
fi
done
Session Security Hardeningβ
Session Fixation Attack Defenseβ
Issue a new session ID after successful login.
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.sessionFixation().migrateSession() // Reissue session ID on login
.maximumSessions(1) // Limit to 1 concurrent session
.maxSessionsPreventsLogin(false) // New login expires previous session
);
return http.build();
}
}
HttpOnly + Secure Cookie Configurationβ
# application.yml
server:
servlet:
session:
cookie:
http-only: true # Block JavaScript access (XSS defense)
secure: true # HTTPS only (prevents HTTP eavesdropping)
same-site: strict # CSRF defense
name: SID # Non-descriptive name instead of JSESSIONID (info hiding)
max-age: 1800
Operations Monitoringβ
# Redis session monitoring script
redis-cli -h redis-host -a password INFO stats | grep -E "keyspace_hits|keyspace_misses"
# Calculate cache hit rate
hits=$(redis-cli INFO stats | grep keyspace_hits | cut -d: -f2)
misses=$(redis-cli INFO stats | grep keyspace_misses | cut -d: -f2)
total=$((hits + misses))
rate=$(echo "scale=2; $hits * 100 / $total" | bc)
echo "Session cache hit rate: $rate%"
# Monitor session count
session_count=$(redis-cli KEYS "session:*" | wc -l)
echo "Active sessions: $session_count"
Use redis_exporter for Prometheus + Grafana automated collection:
# docker-compose.yml
services:
redis-exporter:
image: oliver006/redis_exporter:latest
environment:
- REDIS_ADDR=redis://redis-host:6379
- REDIS_PASSWORD=your-password
ports:
- "9121:9121"