Redis as a Session Store: Patterns and Best Practices
Moving from file-based or database sessions to Redis was a game-changer for our application’s scalability. When we needed to scale horizontally across multiple web servers, Redis became the obvious choice for shared session storage. Here’s everything I learned about implementing Redis sessions in production.
Why Redis for Sessions?
Traditional session storage has limitations:
- File-based: Doesn’t work across multiple servers
- Database: Creates unnecessary load and slower than memory
- Memcached: No persistence, data loss on restart
Redis offers the best of both worlds:
- In-memory performance
- Optional persistence
- Built-in expiration
- Atomic operations
- Replication support
Basic Redis Session Setup
PHP Configuration
// config/session.php
return [
'driver' => 'redis',
'connection' => 'session',
'lifetime' => 120,
'expire_on_close' => false,
];
// config/database.php
'redis' => [
'client' => 'predis',
'session' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 1, // Separate database for sessions
],
],
Node.js/Express Configuration
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redis = require('redis');
const redisClient = redis.createClient({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
password: process.env.REDIS_PASSWORD,
db: 1,
legacyMode: false
});
redisClient.connect().catch(console.error);
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 1000 * 60 * 60 * 24 // 24 hours
}
}));
Session Data Structure
Redis stores sessions as serialized strings with automatic expiration:
Key: session:abc123xyz
Value: {"user_id":42,"username":"john","cart":[1,5,7],"csrf_token":"xyz"}
TTL: 7200 seconds
View sessions in Redis:
# List all session keys
redis-cli KEYS "session:*"
# Get session data
redis-cli GET "session:abc123xyz"
# Check TTL
redis-cli TTL "session:abc123xyz"
Session Management Patterns
1. Sliding Expiration
Extend session lifetime on each request:
// Middleware to refresh session
class RefreshSession
{
public function handle($request, Closure $next)
{
$response = $next($request);
if ($request->session()->has('user_id')) {
// Refresh session TTL
Redis::expire(
'session:' . $request->session()->getId(),
config('session.lifetime') * 60
);
}
return $response;
}
}
2. Session Locking
Prevent race conditions with concurrent requests:
class SessionLock
{
private $redis;
private $lockKey;
private $lockTimeout = 10;
public function acquire($sessionId)
{
$this->lockKey = "lock:session:{$sessionId}";
$acquired = $this->redis->set(
$this->lockKey,
1,
'NX', // Only set if not exists
'EX', // Set expiration
$this->lockTimeout
);
if (!$acquired) {
// Wait and retry
usleep(100000); // 100ms
return $this->acquire($sessionId);
}
return true;
}
public function release()
{
$this->redis->del($this->lockKey);
}
}
3. Multi-Tier Sessions
Store frequently accessed data in local cache:
class TieredSessionHandler
{
private $redis;
private $localCache = [];
public function read($sessionId)
{
// Try local cache first
if (isset($this->localCache[$sessionId])) {
return $this->localCache[$sessionId];
}
// Fallback to Redis
$data = $this->redis->get("session:{$sessionId}");
// Cache locally
$this->localCache[$sessionId] = $data;
return $data;
}
public function write($sessionId, $data)
{
// Update both caches
$this->localCache[$sessionId] = $data;
$this->redis->setex(
"session:{$sessionId}",
1800,
$data
);
}
}
Security Best Practices
1. Secure Session IDs
Generate cryptographically secure session IDs:
function generateSessionId()
{
return bin2hex(random_bytes(32)); // 64 character hex string
}
2. Session Fixation Prevention
Regenerate session ID after authentication:
public function login(Request $request)
{
$credentials = $request->only('email', 'password');
if (Auth::attempt($credentials)) {
// Regenerate session ID
$request->session()->regenerate();
// Store user data
session(['user_id' => Auth::id()]);
return redirect()->intended('dashboard');
}
return back()->withErrors(['email' => 'Invalid credentials']);
}
3. IP Binding
Bind sessions to IP addresses:
class IpBoundSession
{
public function create($userId, $ip)
{
$sessionId = $this->generateId();
$data = [
'user_id' => $userId,
'ip' => $ip,
'created_at' => time()
];
Redis::setex(
"session:{$sessionId}",
3600,
json_encode($data)
);
return $sessionId;
}
public function validate($sessionId, $ip)
{
$data = json_decode(
Redis::get("session:{$sessionId}"),
true
);
if (!$data || $data['ip'] !== $ip) {
throw new SecurityException('Invalid session');
}
return $data;
}
}
Performance Optimization
1. Connection Pooling
Reuse Redis connections:
class RedisConnectionPool
{
private static $connections = [];
private static $maxConnections = 10;
public static function getConnection()
{
if (count(self::$connections) < self::$maxConnections) {
$connection = new Redis();
$connection->connect('127.0.0.1', 6379);
self::$connections[] = $connection;
return $connection;
}
// Return existing connection
return self::$connections[array_rand(self::$connections)];
}
}
2. Lazy Session Loading
Only load session data when needed:
class LazySession
{
private $loaded = false;
private $data = [];
public function get($key)
{
if (!$this->loaded) {
$this->load();
}
return $this->data[$key] ?? null;
}
private function load()
{
$sessionId = $_COOKIE['session_id'] ?? null;
if ($sessionId) {
$data = Redis::get("session:{$sessionId}");
$this->data = json_decode($data, true) ?? [];
}
$this->loaded = true;
}
}
3. Compression
Compress large session data:
class CompressedSession
{
public function write($sessionId, $data)
{
$compressed = gzcompress(serialize($data), 6);
Redis::setex(
"session:{$sessionId}",
3600,
$compressed
);
}
public function read($sessionId)
{
$compressed = Redis::get("session:{$sessionId}");
if (!$compressed) {
return null;
}
return unserialize(gzuncompress($compressed));
}
}
High Availability Setup
Redis Sentinel for Automatic Failover
# sentinel.conf
sentinel monitor mysessions 127.0.0.1 6379 2
sentinel down-after-milliseconds mysessions 5000
sentinel parallel-syncs mysessions 1
sentinel failover-timeout mysessions 10000
Application configuration:
$sentinels = [
['host' => '10.0.1.1', 'port' => 26379],
['host' => '10.0.1.2', 'port' => 26379],
['host' => '10.0.1.3', 'port' => 26379],
];
$redis = new RedisSentinel($sentinels, [
'timeout' => 2,
'persistent' => true,
]);
$master = $redis->getMasterAddrByName('mysessions');
$client = new Redis();
$client->connect($master[0], $master[1]);
Monitoring Sessions
Track Active Sessions
// Store user session mapping
class SessionTracker
{
public function track($userId, $sessionId)
{
// Add to user's session set
Redis::sadd("user:{$userId}:sessions", $sessionId);
// Set with TTL
Redis::expire("user:{$userId}:sessions", 86400);
}
public function getActiveSessions($userId)
{
return Redis::smembers("user:{$userId}:sessions");
}
public function logout($userId, $sessionId)
{
Redis::srem("user:{$userId}:sessions", $sessionId);
Redis::del("session:{$sessionId}");
}
public function logoutAll($userId)
{
$sessions = $this->getActiveSessions($userId);
foreach ($sessions as $sessionId) {
Redis::del("session:{$sessionId}");
}
Redis::del("user:{$userId}:sessions");
}
}
Session Analytics
// Track session statistics
class SessionAnalytics
{
public function recordLogin($userId)
{
$key = "stats:logins:" . date('Y-m-d');
Redis::hincrby($key, $userId, 1);
Redis::expire($key, 86400 * 7); // Keep for 7 days
}
public function getActiveSessionCount()
{
$keys = Redis::keys("session:*");
return count($keys);
}
public function getAverageSessionDuration()
{
$sessions = Redis::keys("session:*");
$total = 0;
foreach ($sessions as $key) {
$ttl = Redis::ttl($key);
if ($ttl > 0) {
$total += (3600 - $ttl); // Assume 1 hour max
}
}
return count($sessions) > 0 ? $total / count($sessions) : 0;
}
}
Session Cleanup
Automatic Cleanup with TTL
Redis automatically removes expired sessions, but you can also manually clean:
class SessionCleanup
{
public function removeExpiredSessions()
{
$pattern = "session:*";
$cursor = 0;
$removed = 0;
do {
$result = Redis::scan($cursor, ['match' => $pattern, 'count' => 100]);
$cursor = $result[0];
$keys = $result[1];
foreach ($keys as $key) {
$ttl = Redis::ttl($key);
// Remove if expired or no TTL set
if ($ttl < 0) {
Redis::del($key);
$removed++;
}
}
} while ($cursor != 0);
return $removed;
}
}
Testing Session Logic
class SessionTest extends TestCase
{
public function setUp(): void
{
parent::setUp();
Redis::flushdb(); // Clean test database
}
public function test_session_creation()
{
$sessionId = $this->createSession(['user_id' => 1]);
$data = Redis::get("session:{$sessionId}");
$this->assertNotNull($data);
$decoded = json_decode($data, true);
$this->assertEquals(1, $decoded['user_id']);
}
public function test_session_expiration()
{
$sessionId = $this->createSession(['user_id' => 1]);
// Check TTL is set
$ttl = Redis::ttl("session:{$sessionId}");
$this->assertGreaterThan(0, $ttl);
$this->assertLessThanOrEqual(3600, $ttl);
}
public function test_concurrent_session_access()
{
$sessionId = $this->createSession(['counter' => 0]);
// Simulate concurrent increments
$promises = [];
for ($i = 0; $i < 10; $i++) {
$promises[] = $this->incrementSessionCounter($sessionId);
}
Promise\all($promises)->wait();
$data = json_decode(Redis::get("session:{$sessionId}"), true);
$this->assertEquals(10, $data['counter']);
}
}
Conclusion
Redis is an excellent choice for session storage in distributed applications:
- Fast in-memory performance
- Built-in expiration handling
- Supports high availability with replication
- Atomic operations prevent race conditions
- Easy to scale horizontally
Key takeaways:
- Use separate Redis database for sessions
- Implement session locking for concurrent access
- Regenerate session IDs after authentication
- Monitor session metrics
- Plan for high availability with Sentinel
Start with the basic setup and add complexity as needed. The patterns shown here will handle millions of sessions efficiently.
Written in April 2016, covering Redis 3.0 session management patterns.