Skip to main content

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​

ConditionSticky SessionTomcat ClusterRedis Session
2 or fewer serversBestPossibleOver-engineering
3~5 serversPossibleBestRecommended
5+ serversNot recommendedNot recommendedRequired
Cloud/KubernetesNot recommendedDifficultRequired
Legacy app (no code changes)BestPossibleDifficult
HA is top priorityNot recommendedPossibleBest
Large session data (1KB+)PossibleNot recommendedBest
Zero-downtime deploymentCautionCautionBest

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();
}
}
# 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"