The Importance of Session Stores
In our previous lecture, we introduced session-based authentication and mentioned the critical role that session stores play. Today, we'll dive deeper into session storage options, configuration best practices, and advanced techniques for production applications.
Real-World Analogy
Think of session stores like different types of record-keeping systems:
- MemoryStore is like keeping records on sticky notes on your desk—convenient but temporary and limited
- Redis Store is like a high-speed filing cabinet with instant access—ideal for frequent retrievals
- MongoDB Store is like a comprehensive archive system—excellent for long-term, structured storage
- Database Stores (MySQL, PostgreSQL) are like traditional filing systems—highly organized but slower to access
The type of storage you choose affects performance, scalability, and data durability.
Default MemoryStore: Limitations and Dangers
Express-session's default MemoryStore is simple but has significant limitations that make it unsuitable for production:
MemoryStore Limitations
- Memory Leaks: The default store has no built-in cleanup mechanism
- Scaling Issues: Sessions are lost when the server restarts
- Single Process: Sessions are only available to the Node.js process that created them
- Size Constraints: Limited by available server memory
- No Persistence: All sessions are lost if the server crashes
Default MemoryStore Implementation
// Basic express-session setup with MemoryStore (not recommended for production)
const express = require('express');
const session = require('express-session');
const app = express();
// This is using the default MemoryStore
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
// WARNING: Memory usage will grow unbounded with this setup!
// The example below shows one way to manually clean up old sessions
const sessionCleanup = () => {
if (session.MemoryStore.sessions) {
const now = Date.now();
Object.keys(session.MemoryStore.sessions).forEach(sid => {
const sessionData = JSON.parse(session.MemoryStore.sessions[sid]);
if (sessionData.cookie && sessionData.cookie.expires) {
const expires = new Date(sessionData.cookie.expires).getTime();
if (expires < now) {
delete session.MemoryStore.sessions[sid];
}
}
});
}
};
// Run cleanup every hour
setInterval(sessionCleanup, 60 * 60 * 1000);
// BUT: This is still not suitable for production!
// - Doesn't work with multiple servers
// - Sessions are lost on server restart
// - Not optimized for high traffic
Production Impact Example
Consider a real-world scenario:
Your e-commerce site suddenly gets featured on a popular blog, bringing 10,000 new visitors in one hour. With the default MemoryStore:
- Each active session might consume 2-10 KB of memory
- 10,000 sessions could consume 20-100 MB of RAM
- Sessions never get cleaned up properly
- After a few days of traffic, your server could run out of memory
- When you deploy a new version, all users get logged out
Redis Session Store
Redis is often considered the gold standard for session storage due to its speed, reliability, and features specifically suited for session management.
Why Redis is Ideal for Session Storage
- In-memory performance: Extremely fast read/write operations
- Persistence options: Can save to disk to survive restarts
- Built-in expiration: Automatic TTL (Time To Live) mechanism
- Atomic operations: Reliable concurrent access
- Clustering support: Horizontal scaling capabilities
- Small footprint: Efficient memory usage
- Pub/Sub features: Useful for real-time applications
Redis Session Store Implementation
// Install required packages
npm install express-session connect-redis redis
// Basic Redis session store implementation
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redis = require('redis');
require('dotenv').config();
const app = express();
// Create Redis client
let redisClient;
// Check if using Redis Cloud, local Redis, or other provider
if (process.env.REDIS_URL) {
// For Redis Cloud, Heroku Redis, etc.
redisClient = redis.createClient({
url: process.env.REDIS_URL,
// If using TLS (like with Redis Cloud)
tls: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : undefined
});
} else {
// For local Redis
redisClient = redis.createClient({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD || ''
});
}
// Handle Redis client errors
redisClient.on('error', (err) => {
console.error('Redis error:', err);
});
// Connect to Redis
redisClient.on('connect', () => {
console.log('Connected to Redis');
});
// Configure session middleware with Redis store
app.use(session({
store: new RedisStore({
client: redisClient,
prefix: 'sess:', // Optional key prefix
ttl: 86400, // Session TTL in seconds (1 day)
disableTouch: false, // Update expiration on session updates
disableTTL: false, // Use TTL for session expiration
}),
secret: process.env.SESSION_SECRET || 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 1 day in milliseconds
}
}));
// ... rest of application code ...
Redis Configuration Options
Important RedisStore Options
- client: Redis client instance
- prefix: Key prefix for session IDs (default: "sess:")
- ttl: Time to live in seconds (default: cookie maxAge or 86400)
- disableTouch: Disable resetting TTL when session is active
- scanCount: Batch size for Redis SCAN operations
- unref: Allow Redis client to exit even with open connections
- serializer: Custom session serialization/deserialization functions
Redis Session Monitoring
// Redis CLI commands for monitoring sessions
// Connect to Redis
redis-cli
// Count total sessions
KEYS sess:*
// View a specific session (replace SESSION_ID)
GET sess:SESSION_ID
// Delete a specific session
DEL sess:SESSION_ID
// Set expiration for a session
EXPIRE sess:SESSION_ID 3600
// View sessions expiring soon (next hour)
redis-cli --eval "return redis.call('KEYS', ARGV[1])" 0 sess:* | xargs -I{} redis-cli TTL {} | awk '$1 > 0 && $1 < 3600 {print $1}'
// Monitor session operations in real-time
MONITOR
Redis Clustering for High Availability
// Redis Cluster setup for high availability
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const Redis = require('ioredis'); // Better for clustering
const app = express();
// Create Redis cluster client
const redisClient = new Redis.Cluster([
{
port: 6380,
host: process.env.REDIS_NODE_1 || 'redis-node-1'
},
{
port: 6380,
host: process.env.REDIS_NODE_2 || 'redis-node-2'
},
{
port: 6380,
host: process.env.REDIS_NODE_3 || 'redis-node-3'
}
], {
redisOptions: {
password: process.env.REDIS_PASSWORD
},
scaleReads: 'all', // Read from all nodes
clusterRetryStrategy: (times) => Math.min(times * 100, 3000) // Retry strategy
});
// Session with Redis cluster store
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: 24 * 60 * 60 * 1000
}
}));
MongoDB Session Store
MongoDB is another excellent option for session storage, especially when your application already uses MongoDB for other data.
Advantages of MongoDB for Session Storage
- Document structure: Natural fit for JSON-like session data
- Automatic expiration: TTL indexes for session cleanup
- Query capabilities: Advanced querying for session analysis
- Scalability: Sharding for horizontal scaling
- Integration: Easy if already using MongoDB
- Replication: Built-in data redundancy
MongoDB Session Store Implementation
// Install required packages
npm install express-session connect-mongo
// Basic MongoDB session store implementation
const express = require('express');
const session = require('express-session');
const MongoStore = require('connect-mongo');
require('dotenv').config();
const app = express();
// Configure session middleware with MongoDB store
app.use(session({
store: MongoStore.create({
mongoUrl: process.env.MONGODB_URI || 'mongodb://localhost:27017/sessions',
collectionName: 'sessions', // Optional (default: 'sessions')
ttl: 14 * 24 * 60 * 60, // Session TTL in seconds (14 days)
autoRemove: 'native', // Use MongoDB's TTL index
touchAfter: 24 * 3600, // Only update session every 24 hours if not modified
crypto: {
secret: process.env.MONGO_STORE_SECRET // Optional encryption
},
autoReconnect: true,
stringify: false // Don't stringify session if not needed
}),
secret: process.env.SESSION_SECRET || 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 14 * 24 * 60 * 60 * 1000 // 14 days in milliseconds
}
}));
// ... rest of application code ...
MongoDB Configuration Options
Important MongoStore Options
- mongoUrl: MongoDB connection string
- collectionName: Collection to store sessions (default: "sessions")
- ttl: Time to live in seconds (default: 14 days)
- autoRemove: Strategy for removing expired sessions
- touchAfter: Minimum time between session updates
- crypto: Enable encryption for session data
- stringify: Whether to JSON stringify session data
Analyzing Sessions in MongoDB
// MongoDB queries for session analysis
// Connect to MongoDB shell
mongo mongodb://localhost:27017/sessions
// Count total sessions
db.sessions.countDocuments()
// Find sessions expiring soon (next hour)
db.sessions.find({
expires: {
$gt: new Date(),
$lt: new Date(new Date().getTime() + 60 * 60 * 1000)
}
})
// Find a specific user's session (if storing userId in session)
db.sessions.find({
"session.userId": ObjectId("60d5ec9ac80b2a2d1c2d5a6b")
})
// Find all admin sessions
db.sessions.find({
"session.userRole": "admin"
})
// Delete expired sessions manually (if TTL index isn't working)
db.sessions.deleteMany({
expires: { $lt: new Date() }
})
// Create or update TTL index
db.sessions.createIndex(
{ expires: 1 },
{ expireAfterSeconds: 0 }
)
MongoDB Advanced Configuration
// Advanced MongoDB session store with replica set
const express = require('express');
const session = require('express-session');
const MongoStore = require('connect-mongo');
const mongoose = require('mongoose');
require('dotenv').config();
const app = express();
// Connect to MongoDB with Mongoose (optional)
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
replicaSet: 'rs0', // If using replica set
readPreference: 'secondaryPreferred' // Read from secondaries when possible
}).then(() => {
console.log('Connected to MongoDB');
}).catch(err => {
console.error('MongoDB connection error:', err);
});
// Configure session with advanced options
app.use(session({
store: MongoStore.create({
// Can use existing mongoose connection
clientPromise: mongoose.connection.asPromise().then(connection => connection.getClient()),
collectionName: 'sessions',
ttl: 14 * 24 * 60 * 60,
autoRemove: 'native',
touchAfter: 24 * 3600,
// You can transform session data before saving/after retrieving
serialize: (session) => {
// Don't store sensitive data
const { password, creditCard, ...sessionData } = session;
return sessionData;
},
// Configure retry behavior
connectionOptions: {
serverSelectionTimeoutMS: 10000,
socketTimeoutMS: 45000,
}
}),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 14 * 24 * 60 * 60 * 1000
}
}));
SQL Database Session Stores
SQL databases like MySQL, PostgreSQL, and SQLite can also be used for session storage, especially in applications that already use these databases.
SQL Store Options
| Database | Package | Features |
|---|---|---|
| MySQL | express-mysql-session | Connection pooling, automatic table creation, configurable schema |
| PostgreSQL | connect-pg-simple | Lightweight, uses pg pool, session cleanup |
| SQLite | connect-sqlite3 | Good for development, file-based storage |
| Any (Sequelize) | connect-session-sequelize | Works with any Sequelize-supported database |
MySQL Session Store Example
// Install required packages
npm install express-session express-mysql-session
// MySQL session store implementation
const express = require('express');
const session = require('express-session');
const MySQLStore = require('express-mysql-session')(session);
require('dotenv').config();
const app = express();
// Database options
const options = {
host: process.env.MYSQL_HOST || 'localhost',
port: process.env.MYSQL_PORT || 3306,
user: process.env.MYSQL_USER || 'root',
password: process.env.MYSQL_PASSWORD || '',
database: process.env.MYSQL_DATABASE || 'session_store',
// Additional options
createDatabaseTable: true, // Auto-create table
schema: {
tableName: 'sessions',
columnNames: {
session_id: 'session_id',
expires: 'expires',
data: 'data'
}
},
clearExpired: true, // Automatically clear expired sessions
checkExpirationInterval: 900000, // Check expiration every 15 minutes (ms)
expiration: 86400000, // Session expiration time (ms)
endConnectionOnClose: true, // End connection when store is closed
charset: 'utf8mb4_unicode_ci', // Charset and collation
connectionLimit: 10 // Connection pool size
};
// Create store
const sessionStore = new MySQLStore(options);
// Handle store errors
sessionStore.on('error', function(error) {
console.error('Session store error:', error);
});
// Configure session middleware
app.use(session({
key: 'session_cookie_name',
secret: process.env.SESSION_SECRET || 'your-secret-key',
store: sessionStore,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
// ... rest of application code ...
PostgreSQL Session Store Example
// Install required packages
npm install express-session connect-pg-simple pg
// PostgreSQL session store implementation
const express = require('express');
const session = require('express-session');
const pgSession = require('connect-pg-simple')(session);
const { Pool } = require('pg');
require('dotenv').config();
const app = express();
// Create PostgreSQL pool
const pool = new Pool({
user: process.env.PGUSER || 'postgres',
host: process.env.PGHOST || 'localhost',
database: process.env.PGDATABASE || 'postgres',
password: process.env.PGPASSWORD || '',
port: process.env.PGPORT || 5432,
ssl: process.env.NODE_ENV === 'production' ?
{ rejectUnauthorized: false } : undefined
});
// Init session with PG store
app.use(session({
store: new pgSession({
pool: pool,
tableName: 'session', // Optional. Default is 'session'
schemaName: 'public', // Optional. Default is 'public'
createTableIfMissing: true, // Create table if it doesn't exist
pruneSessionInterval: 60 * 15 // 15 minutes
}),
secret: process.env.SESSION_SECRET || 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
}
}));
// ... rest of application code ...
Required PostgreSQL Table Schema
If not using createTableIfMissing: true, you'll need to create the sessions table:
CREATE TABLE "session" (
"sid" varchar NOT NULL COLLATE "default",
"sess" json NOT NULL,
"expire" timestamp(6) NOT NULL
)
WITH (OIDS=FALSE);
ALTER TABLE "session" ADD CONSTRAINT "session_pkey"
PRIMARY KEY ("sid") NOT DEFERRABLE INITIALLY IMMEDIATE;
CREATE INDEX "IDX_session_expire" ON "session" ("expire");
Custom Session Stores
For specialized requirements, you can create custom session stores by implementing the required session store interface.
The Session Store Interface
A session store must implement at least these methods:
- get(sid, callback): Retrieve session data
- set(sid, session, callback): Save session data
- destroy(sid, callback): Delete session data
Optional but recommended methods:
- touch(sid, session, callback): Update session expiration
- all(callback): Get all sessions
- length(callback): Get session count
- clear(callback): Delete all sessions
Creating a Custom File Store
// Example: Simple file-based session store
const fs = require('fs').promises;
const path = require('path');
const { promisify } = require('util');
const session = require('express-session');
class FileStore extends session.Store {
constructor(options = {}) {
super();
this.path = options.path || path.join(process.cwd(), 'sessions');
this.ttl = options.ttl || 86400; // 1 day in seconds
this.init();
}
async init() {
try {
await fs.mkdir(this.path, { recursive: true });
console.log(`Session directory created at: ${this.path}`);
// Optional: Set up a cleanup interval
if (this.ttl > 0) {
this.startCleanup();
}
} catch (err) {
console.error('Error initializing FileStore:', err);
}
}
// Helper to get file path for a session ID
getFilePath(sid) {
return path.join(this.path, `${sid}.json`);
}
// Get session data
get(sid, callback) {
const filePath = this.getFilePath(sid);
fs.readFile(filePath, 'utf8')
.then(data => {
let sessionData;
try {
sessionData = JSON.parse(data);
// Check if session has expired
if (sessionData.__expires && sessionData.__expires < Date.now()) {
this.destroy(sid, () => {});
return callback(null, null);
}
delete sessionData.__expires;
callback(null, sessionData);
} catch (err) {
callback(err);
}
})
.catch(err => {
// File doesn't exist or can't be read
if (err.code === 'ENOENT') {
return callback(null, null);
}
callback(err);
});
}
// Set session data
set(sid, session, callback) {
const filePath = this.getFilePath(sid);
// Add expiration timestamp
const sessionData = { ...session };
if (session.cookie && session.cookie.expires) {
sessionData.__expires = new Date(session.cookie.expires).getTime();
} else if (this.ttl > 0) {
sessionData.__expires = Date.now() + (this.ttl * 1000);
}
fs.writeFile(filePath, JSON.stringify(sessionData), 'utf8')
.then(() => callback(null))
.catch(callback);
}
// Destroy session data
destroy(sid, callback) {
const filePath = this.getFilePath(sid);
fs.unlink(filePath)
.then(() => callback(null))
.catch(err => {
// Don't treat file not found as an error
if (err.code === 'ENOENT') {
return callback(null);
}
callback(err);
});
}
// Touch - update session expiration
touch(sid, session, callback) {
this.get(sid, (err, data) => {
if (err) return callback(err);
if (!data) return callback();
this.set(sid, session, callback);
});
}
// Get all sessions
all(callback) {
fs.readdir(this.path)
.then(files => {
const sessionPromises = files
.filter(file => file.endsWith('.json'))
.map(file => {
const sid = file.slice(0, -5); // Remove .json
return new Promise((resolve, reject) => {
this.get(sid, (err, session) => {
if (err) return reject(err);
if (!session) return resolve(null); // Expired or invalid
resolve({ sid, session });
});
});
});
Promise.all(sessionPromises)
.then(results => {
const sessions = results
.filter(Boolean)
.reduce((acc, { sid, session }) => {
acc[sid] = session;
return acc;
}, {});
callback(null, sessions);
})
.catch(callback);
})
.catch(callback);
}
// Get session count
length(callback) {
this.all((err, sessions) => {
if (err) return callback(err);
callback(null, Object.keys(sessions).length);
});
}
// Delete all sessions
clear(callback) {
fs.readdir(this.path)
.then(files => {
const unlinkPromises = files
.filter(file => file.endsWith('.json'))
.map(file => fs.unlink(path.join(this.path, file)));
Promise.all(unlinkPromises)
.then(() => callback(null))
.catch(callback);
})
.catch(callback);
}
// Start cleanup process
startCleanup() {
const cleanup = async () => {
try {
const files = await fs.readdir(this.path);
for (const file of files) {
if (!file.endsWith('.json')) continue;
const filePath = path.join(this.path, file);
try {
const data = await fs.readFile(filePath, 'utf8');
const session = JSON.parse(data);
if (session.__expires && session.__expires < Date.now()) {
await fs.unlink(filePath);
}
} catch (err) {
console.error(`Error cleaning up session ${file}:`, err);
}
}
} catch (err) {
console.error('Session cleanup error:', err);
}
};
// Run cleanup every hour
setInterval(cleanup, 60 * 60 * 1000);
// Run initial cleanup
cleanup();
}
}
module.exports = FileStore;
Using the Custom Store
// Using the custom file store
const express = require('express');
const session = require('express-session');
const FileStore = require('./file-store');
const app = express();
app.use(session({
store: new FileStore({
path: './sessions',
ttl: 86400 // 1 day in seconds
}),
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000 // 1 day
}
}));
Session Store Selection Criteria
Choosing the right session store depends on several factors:
| Factor | MemoryStore | Redis | MongoDB | SQL Database |
|---|---|---|---|---|
| Performance | Very High | High | Medium | Medium-Low |
| Scalability | Poor | Excellent | Good | Good |
| Persistence | None | Optional | Yes | Yes |
| Data Structure | Any | Key-Value | Document | Relational |
| Setup Complexity | Very Low | Low-Medium | Medium | Medium |
| Query Capabilities | None | Limited | Advanced | Advanced |
| Best For | Development | Production | Complex Data | Existing SQL Apps |
Decision Guidelines
- Choose Redis for most production applications, especially those with high traffic
- Choose MongoDB if you're already using MongoDB and need complex session data
- Choose SQL Database if you're using a relational database and have lower session volume
- Choose MemoryStore only for development or very small applications
- Consider custom stores for specialized requirements or unusual infrastructure
Advanced Session Configuration
Session Serialization and Compression
Customizing how session data is stored can improve performance and security:
// Custom serialization and compression for Redis store
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redis = require('redis');
const zlib = require('zlib');
const redisClient = redis.createClient();
// Custom serializer with compression
const customSerializer = {
// Serialize and compress session
stringify: (session) => {
// Remove sensitive data
const { password, creditCard, ...sessionData } = session;
// Convert to JSON string
const jsonString = JSON.stringify(sessionData);
// Compress only if session is large
if (jsonString.length > 1000) {
const compressed = zlib.deflateSync(jsonString);
return 'c:' + compressed.toString('base64');
}
return 'u:' + jsonString;
},
// Deserialize and decompress session
parse: (rawData) => {
if (!rawData) return null;
// Check if data is compressed
if (rawData.startsWith('c:')) {
const compressed = Buffer.from(rawData.slice(2), 'base64');
const decompressed = zlib.inflateSync(compressed);
return JSON.parse(decompressed.toString());
}
// Uncompressed data
if (rawData.startsWith('u:')) {
return JSON.parse(rawData.slice(2));
}
// For backwards compatibility
try {
return JSON.parse(rawData);
} catch (err) {
return null;
}
}
};
// Configure session
app.use(session({
store: new RedisStore({
client: redisClient,
serializer: customSerializer,
prefix: 'sess:'
}),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000
}
}));
Session Encryption
For additional security, you can encrypt session data:
// Encrypted sessions with MongoDB store
const express = require('express');
const session = require('express-session');
const MongoStore = require('connect-mongo');
const crypto = require('crypto');
const app = express();
// Encryption configuration
const ENCRYPTION_KEY = process.env.SESSION_ENCRYPTION_KEY; // Must be 32 bytes
const IV_LENGTH = 16; // For AES, this is always 16
// Encryption/decryption functions
const encrypt = (text) => {
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv(
'aes-256-cbc',
Buffer.from(ENCRYPTION_KEY, 'hex'),
iv
);
let encrypted = cipher.update(text);
encrypted = Buffer.concat([encrypted, cipher.final()]);
return iv.toString('hex') + ':' + encrypted.toString('hex');
};
const decrypt = (text) => {
const parts = text.split(':');
const iv = Buffer.from(parts[0], 'hex');
const encryptedText = Buffer.from(parts[1], 'hex');
const decipher = crypto.createDecipheriv(
'aes-256-cbc',
Buffer.from(ENCRYPTION_KEY, 'hex'),
iv
);
let decrypted = decipher.update(encryptedText);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString();
};
// Custom serialization with encryption
const customSerializer = {
serialize: (session) => {
return encrypt(JSON.stringify(session));
},
deserialize: (sessionData) => {
return JSON.parse(decrypt(sessionData));
}
};
// Session configuration with MongoDB and encryption
app.use(session({
store: MongoStore.create({
mongoUrl: process.env.MONGODB_URI,
collectionName: 'encrypted_sessions',
ttl: 24 * 60 * 60, // 1 day
autoRemove: 'native',
serialize: customSerializer.serialize,
unserialize: customSerializer.deserialize
}),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000
}
}));
Session Store with Fallback
For high availability, you can implement a fallback mechanism:
// Session store with Redis primary and MongoDB fallback
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const MongoStore = require('connect-mongo');
const redis = require('redis');
require('dotenv').config();
const app = express();
// Create Redis client
const redisClient = redis.createClient({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD || ''
});
// Create primary store
const primaryStore = new RedisStore({
client: redisClient,
prefix: 'sess:'
});
// Create fallback store
const fallbackStore = MongoStore.create({
mongoUrl: process.env.MONGODB_URI,
collectionName: 'sessions',
ttl: 24 * 60 * 60
});
// Custom store with fallback
class FallbackStore extends session.Store {
constructor(options) {
super();
this.primary = options.primary;
this.fallback = options.fallback;
this.primaryAvailable = true;
// Monitor Redis connection
if (this.primary.client) {
this.primary.client.on('error', (err) => {
console.error('Primary store error:', err);
this.primaryAvailable = false;
});
this.primary.client.on('connect', () => {
console.log('Primary store connected');
this.primaryAvailable = true;
});
}
}
get(sid, callback) {
if (this.primaryAvailable) {
this.primary.get(sid, (err, session) => {
if (err || !session) {
// Try fallback
return this.fallback.get(sid, callback);
}
callback(null, session);
});
} else {
this.fallback.get(sid, callback);
}
}
set(sid, session, callback) {
// Always try to save to both stores
this.fallback.set(sid, session, (fallbackErr) => {
if (!this.primaryAvailable) {
return callback(fallbackErr);
}
this.primary.set(sid, session, (primaryErr) => {
callback(primaryErr || fallbackErr);
});
});
}
destroy(sid, callback) {
// Delete from both stores
this.fallback.destroy(sid, (fallbackErr) => {
if (!this.primaryAvailable) {
return callback(fallbackErr);
}
this.primary.destroy(sid, (primaryErr) => {
callback(primaryErr || fallbackErr);
});
});
}
touch(sid, session, callback) {
if (this.primaryAvailable) {
this.primary.touch(sid, session, (err) => {
if (err) {
return this.fallback.touch(sid, session, callback);
}
// Also touch in fallback for consistency
this.fallback.touch(sid, session, () => {
callback(null);
});
});
} else {
this.fallback.touch(sid, session, callback);
}
}
}
// Use the fallback store
app.use(session({
store: new FallbackStore({
primary: primaryStore,
fallback: fallbackStore
}),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000
}
}));
Session Store Performance Optimization
Redis Performance Optimization
// Optimized Redis session configuration
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redis = require('redis');
const app = express();
// Optimized Redis client
const redisClient = redis.createClient({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
password: process.env.REDIS_PASSWORD,
db: 0, // Use database 0 for sessions
// Performance options
retry_strategy: function(options) {
if (options.error && options.error.code === 'ECONNREFUSED') {
return new Error('Redis server refused connection');
}
if (options.total_retry_time > 1000 * 60) {
return new Error('Redis retry time exhausted');
}
if (options.attempt > 10) {
return undefined; // Stop retrying
}
return Math.min(options.attempt * 100, 3000); // Increasing delay
},
detect_buffers: false, // Disable for performance
socket_keepalive: true, // Keep connection alive
socket_initial_delay: 10000 // Delay for keepalive packets
});
// Optimize Redis store
app.use(session({
store: new RedisStore({
client: redisClient,
prefix: 'sess:',
ttl: 86400,
disableTTL: false,
disableTouch: false,
scanCount: 100, // Batch size for SCAN operations
// Don't save entire session if only specific fields changed
saveUninitialized: false,
unref: true, // Allow shutdown even with connections
// Use JSON.stringify for serialization (default)
serializer: undefined
}),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
rolling: false, // Disable to reduce Redis writes
cookie: {
maxAge: 24 * 60 * 60 * 1000
}
}));
// For read-heavy scenarios, implement caching
const sessionCache = new Map();
const SESSION_CACHE_TTL = 60 * 1000; // 1 minute
// Middleware to cache session data
app.use((req, res, next) => {
const originalEnd = res.end;
// Cache session data when response ends
res.end = function(...args) {
if (req.session) {
const sessionId = req.sessionID;
sessionCache.set(sessionId, {
data: { ...req.session },
expires: Date.now() + SESSION_CACHE_TTL
});
// Schedule cleanup
setTimeout(() => {
sessionCache.delete(sessionId);
}, SESSION_CACHE_TTL);
}
originalEnd.apply(this, args);
};
next();
});
MongoDB Performance Optimization
// Optimized MongoDB session configuration
const express = require('express');
const session = require('express-session');
const MongoStore = require('connect-mongo');
const mongoose = require('mongoose');
const app = express();
// Optimize Mongoose connection (if using Mongoose)
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
// Performance options
poolSize: 10, // Connection pool size
connectTimeoutMS: 10000, // Connection timeout
socketTimeoutMS: 45000, // Socket timeout
serverSelectionTimeoutMS: 5000, // Server selection timeout
// Read preference for scaling
readPreference: 'secondaryPreferred',
// Write concern
w: 'majority',
wtimeout: 2500
});
// Optimize MongoDB store
app.use(session({
store: MongoStore.create({
mongoUrl: process.env.MONGODB_URI,
collectionName: 'sessions',
ttl: 24 * 60 * 60,
autoRemove: 'native',
touchAfter: 24 * 3600, // Update only if session changed
stringify: false, // Don't stringify if not needed
// Connection options
connectionOptions: {
useNewUrlParser: true,
useUnifiedTopology: true,
poolSize: 5
},
// Crypto options for security
crypto: {
secret: process.env.MONGO_STORE_SECRET
}
}),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000
}
}));
// Create indexes for MongoDB session collection
// Run this during application startup
const createSessionIndexes = async () => {
try {
const db = mongoose.connection.db;
const collection = db.collection('sessions');
// Check if indexes exist
const indexes = await collection.indexes();
const expiryIndexExists = indexes.some(
index => index.name === 'expires_1'
);
if (!expiryIndexExists) {
// Create TTL index
await collection.createIndex(
{ expires: 1 },
{ expireAfterSeconds: 0, name: 'expires_1' }
);
console.log('Created TTL index for sessions');
}
// Create additional indexes for performance
await collection.createIndex(
{ session: 1 },
{ name: 'session_1', sparse: true }
);
console.log('Session indexes verified');
} catch (err) {
console.error('Error creating session indexes:', err);
}
};
// Call after MongoDB connection established
mongoose.connection.once('connected', createSessionIndexes);
Session Data Optimization
// Optimizing session data size and structure
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redis = require('redis');
const app = express();
const redisClient = redis.createClient();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000
}
}));
// Middleware to optimize session data
app.use((req, res, next) => {
// Original session data getter/setter
const originalGet = req.session.get;
const originalSet = req.session.set;
// Track changes to session
let sessionModified = false;
// Only store minimal data in session
if (req.session.userId && !req.session.user && req.path !== '/profile') {
// Don't load full user data until needed
delete req.session.user;
}
// Clean large objects from session
if (req.session.searchResults) {
// Store only IDs, not full result objects
req.session.searchResultIds = req.session.searchResults.map(r => r.id);
delete req.session.searchResults;
sessionModified = true;
}
// Use short keys to reduce storage
if (req.session.userPreferences) {
req.session.prefs = req.session.userPreferences;
delete req.session.userPreferences;
sessionModified = true;
}
// Handle case-specific optimizations
if (req.session.shoppingCart && req.session.shoppingCart.items) {
// Store minimal cart data (just product IDs and quantities)
req.session.cart = req.session.shoppingCart.items.map(item => ({
id: item.productId,
q: item.quantity
}));
delete req.session.shoppingCart;
sessionModified = true;
}
// Force session save if modified
if (sessionModified) {
req.session.save();
}
next();
});
Monitoring and Debugging Session Stores
Monitoring Redis Session Store
// Redis session monitoring
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redis = require('redis');
const { performance } = require('perf_hooks');
const app = express();
const redisClient = redis.createClient();
// Create monitoring Redis client (separate from session store client)
const monitorClient = redis.createClient({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
password: process.env.REDIS_PASSWORD
});
// Monitor Redis store performance
const sessionStore = new RedisStore({
client: redisClient,
prefix: 'sess:',
ttl: 86400
});
// Add performance monitors to original methods
const originalGet = sessionStore.get;
sessionStore.get = function(sid, callback) {
const start = performance.now();
originalGet.call(this, sid, (err, session) => {
const duration = performance.now() - start;
// Log slow operations
if (duration > 50) { // 50ms threshold
console.warn(`Slow session.get: ${duration.toFixed(2)}ms for ${sid}`);
}
// Track metrics
if (global.sessionMetrics) {
global.sessionMetrics.gets++;
global.sessionMetrics.getTotalTime += duration;
}
callback(err, session);
});
};
// Similar instrumentation for set, destroy, etc.
// Global metrics object
global.sessionMetrics = {
gets: 0,
getTotalTime: 0,
sets: 0,
setTotalTime: 0,
destroys: 0,
// Reset every minute
reset: function() {
Object.keys(this).forEach(key => {
if (typeof this[key] === 'number') {
this[key] = 0;
}
});
}
};
// Reset metrics every minute
setInterval(() => {
if (global.sessionMetrics) {
console.log('Session metrics:', { ...global.sessionMetrics });
global.sessionMetrics.reset();
}
}, 60 * 1000);
// Session monitoring endpoints
app.get('/admin/session-stats', async (req, res) => {
try {
// Get total sessions
monitorClient.keys('sess:*', (err, keys) => {
if (err) {
return res.status(500).json({ error: err.message });
}
const stats = {
totalSessions: keys.length,
metrics: { ...global.sessionMetrics }
};
if (stats.metrics.gets > 0) {
stats.metrics.avgGetTime =
stats.metrics.getTotalTime / stats.metrics.gets;
}
if (stats.metrics.sets > 0) {
stats.metrics.avgSetTime =
stats.metrics.setTotalTime / stats.metrics.sets;
}
res.json(stats);
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.use(session({
store: sessionStore,
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000
}
}));
Debugging Session Issues
// Session debugging middleware
const debugSession = (req, res, next) => {
// Only enable in development
if (process.env.NODE_ENV !== 'development') {
return next();
}
// Log session operations
console.log(`[${new Date().toISOString()}] Session ID: ${req.sessionID}`);
if (!req.session) {
console.warn('No session object found!');
return next();
}
// Track original session properties
const originalSession = JSON.parse(JSON.stringify(req.session));
// Override session save method
const originalSave = req.session.save;
req.session.save = function(callback) {
console.log('Session save called');
// Find changed properties
const newProps = {};
const deletedProps = {};
Object.keys(req.session).forEach(key => {
if (key !== 'cookie' && (!originalSession[key] ||
JSON.stringify(originalSession[key]) !== JSON.stringify(req.session[key]))) {
newProps[key] = req.session[key];
}
});
Object.keys(originalSession).forEach(key => {
if (key !== 'cookie' && req.session[key] === undefined) {
deletedProps[key] = originalSession[key];
}
});
if (Object.keys(newProps).length > 0) {
console.log('Changed/added session properties:', newProps);
}
if (Object.keys(deletedProps).length > 0) {
console.log('Deleted session properties:', deletedProps);
}
return originalSave.call(this, callback);
};
// Track when session is destroyed
const originalDestroy = req.session.destroy;
req.session.destroy = function(callback) {
console.log('Session destroy called');
return originalDestroy.call(this, callback);
};
// Track response end to see final session state
const originalEnd = res.end;
res.end = function(...args) {
console.log('Final session state:', req.session);
return originalEnd.apply(this, args);
};
next();
};
// Apply debugging middleware
app.use(debugSession);
// After session middleware
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000
}
}));
Best Practices for Session Stores in Production
Security Best Practices
- Encrypt sensitive session data before storing
- Use network isolation for session stores (private subnets, VPC)
- Implement authentication for Redis/MongoDB connections
- Enable TLS/SSL for connections to session stores
- Use separate databases/instances for sessions and application data
- Sanitize session data - never store raw user input
- Implement session timeouts - both idle and absolute
- Monitor for unusual access patterns to detect attacks
Performance Best Practices
- Store minimal data in sessions - only what's necessary
- Use connection pooling for database session stores
- Enable proper indexes for MongoDB or SQL session collections/tables
- Set appropriate TTL for session expiration
- Use the touchAfter option to reduce write operations
- Consider caching for frequently accessed but rarely changed sessions
- Monitor session store performance with metrics
- Use appropriate serialization for your data patterns
Reliability Best Practices
- Set up replication/clustering for Redis or MongoDB
- Implement health checks for session stores
- Configure proper backup procedures for session data
- Implement graceful degradation if session store is temporarily unavailable
- Test session store failure scenarios before production
- Implement circuit breakers for session store operations
- Consider multi-region replication for global applications
- Have a session recovery strategy for worst-case scenarios
Practice Activities
Activity 1: Redis Session Store Implementation
Set up a complete Redis session store for an Express.js application:
- Install Redis and the required Node.js packages
- Configure Express.js with Redis session store
- Implement session creation, retrieval, and expiration
- Create a test route that increments a counter in the session
- Create another route that displays all active sessions (for admins)
- Test persistence by restarting the server and verifying sessions remain
Bonus: Implement Redis Sentinel or Redis Cluster for high availability
Activity 2: Session Store Comparison
Create a benchmarking application to compare different session stores:
- Implement the same Express app with three different stores: MemoryStore, Redis, and MongoDB
- Create a load testing script that simulates multiple concurrent users
- Measure and compare performance metrics:
- Response time
- Memory usage
- CPU usage
- Session creation speed
- Session retrieval speed
- Test with different session sizes and user loads
- Create a report analyzing the strengths and weaknesses of each store
Activity 3: Custom Session Store with Encryption
Build a custom session store that adds encryption to session data:
- Implement a custom session store class that extends the base Store class
- Add encryption/decryption for all session data
- Implement the required get, set, destroy methods
- Add optional touch, all, length, clear methods
- Choose a backend storage (file system, Redis, etc.)
- Test the store with an Express application
- Verify encrypted data in the storage backend
Activity 4: Session Store Monitoring Dashboard
Create a monitoring dashboard for session stores:
- Implement metrics collection for session operations:
- Session count
- Session creation rate
- Session expiration rate
- Average session size
- Operation latency (get, set, destroy)
- Create a real-time dashboard using WebSockets
- Add alerts for abnormal conditions:
- High session creation rate (possible DoS)
- Increasing average session size
- High operation latency
- Unusual session patterns
- Implement session store health checks
- Add historical metrics for trend analysis
Handling Edge Cases and Common Problems
Session Store Connection Failures
Implementing resilient error handling for session store connection issues:
// Handling session store connection failures
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redis = require('redis');
const app = express();
// Create Redis client with retry strategy
const redisClient = redis.createClient({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD || '',
retry_strategy: function(options) {
if (options.error && options.error.code === 'ECONNREFUSED') {
// Server refused connection, maybe it's down
console.error('Redis connection refused. Retrying...');
return Math.min(options.attempt * 100, 3000);
}
if (options.total_retry_time > 1000 * 60 * 60) {
// Retry for maximum 1 hour
console.error('Redis retry time exhausted. Using fallback.');
return undefined;
}
if (options.attempt > 10) {
// Try 10 times then switch to fallback
console.error('Redis max retry attempts reached. Using fallback.');
return undefined;
}
// Exponential backoff with cap
return Math.min(options.attempt * 100, 3000);
}
});
// Handle Redis client errors
redisClient.on('error', (err) => {
console.error('Redis error:', err);
});
// Create session store with fallback
let sessionStore;
try {
sessionStore = new RedisStore({ client: redisClient });
// Add error handler to store
sessionStore.on('error', (err) => {
console.error('Session store error:', err);
});
} catch (error) {
console.error('Failed to create Redis session store:', error);
console.warn('Falling back to MemoryStore (not recommended for production)');
sessionStore = new session.MemoryStore();
}
// Configure session middleware with graceful fallback
app.use((req, res, next) => {
session({
store: sessionStore,
secret: process.env.SESSION_SECRET || 'fallback-secret',
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000
}
})(req, res, (err) => {
if (err) {
// Session middleware failed
console.error('Session middleware error:', err);
// Attach empty session to prevent app crashes
if (!req.session) {
req.session = {};
req.sessionID = Date.now().toString();
}
// Set a flag to indicate degraded session functionality
req.sessionDegraded = true;
// Continue to next middleware
return next();
}
next();
});
});
Handling Large Sessions
Strategies for dealing with large session data:
// Handling large session data
const express = require('express');
const session = require('express-session');
const MongoStore = require('connect-mongo');
const compression = require('compression');
const app = express();
// Apply compression middleware
app.use(compression());
// Session size monitoring middleware
app.use((req, res, next) => {
const originalWrite = res.write;
const originalEnd = res.end;
// Function to check session size
const checkSessionSize = () => {
if (!req.session) return;
// Get session size
const sessionSize = JSON.stringify(req.session).length;
// Log and handle large sessions
if (sessionSize > 100 * 1024) { // 100 KB
console.error(`Extremely large session detected: ${sessionSize} bytes, ID: ${req.sessionID}`);
// Find largest keys in session
const sessionKeys = Object.keys(req.session)
.filter(key => key !== 'cookie')
.map(key => ({
key,
size: JSON.stringify(req.session[key]).length
}))
.sort((a, b) => b.size - a.size);
console.warn('Largest session keys:', sessionKeys.slice(0, 5));
// Consider deleting very large keys
sessionKeys.forEach(({ key, size }) => {
if (size > 50 * 1024) { // 50 KB
console.warn(`Removing large session key: ${key} (${size} bytes)`);
delete req.session[key];
}
});
}
else if (sessionSize > 50 * 1024) { // 50 KB
console.warn(`Large session detected: ${sessionSize} bytes, ID: ${req.sessionID}`);
}
};
// Check on response finish
res.end = function(...args) {
checkSessionSize();
originalEnd.apply(this, args);
};
next();
});
// Session with storage optimization
app.use(session({
store: MongoStore.create({
mongoUrl: process.env.MONGODB_URI,
collectionName: 'sessions',
ttl: 24 * 60 * 60,
// Compress sessions larger than 1 KB
serialize: (session) => {
const sessionStr = JSON.stringify(session);
// Don't compress small sessions
if (sessionStr.length < 1024) {
return sessionStr;
}
// Use compression for larger sessions
const zlib = require('zlib');
const compressed = zlib.deflateSync(sessionStr);
return 'compressed:' + compressed.toString('base64');
},
// Deserialize and decompress if needed
unserialize: (sessionStr) => {
if (!sessionStr) return null;
// Check if compressed
if (sessionStr.startsWith('compressed:')) {
const zlib = require('zlib');
const compressed = Buffer.from(sessionStr.slice(11), 'base64');
const decompressed = zlib.inflateSync(compressed);
return JSON.parse(decompressed.toString());
}
// Regular session
return JSON.parse(sessionStr);
}
}),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000
}
}));
Handling Session Store Unavailability
Strategies for dealing with temporary session store outages:
// Handling session store unavailability
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redis = require('redis');
const app = express();
// Create Redis client
const redisClient = redis.createClient({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
password: process.env.REDIS_PASSWORD
});
// Session store health flag
let sessionStoreHealthy = true;
// Monitor Redis connection
redisClient.on('error', (err) => {
console.error('Redis error:', err);
sessionStoreHealthy = false;
});
redisClient.on('connect', () => {
console.log('Redis connected');
sessionStoreHealthy = true;
});
// Create session store
const sessionStore = new RedisStore({ client: redisClient });
// Session middleware with degradation handling
app.use((req, res, next) => {
// Normal session handling
session({
store: sessionStore,
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000
}
})(req, res, (err) => {
// Handle session errors
if (err || !sessionStoreHealthy) {
// Log the error
if (err) {
console.error('Session error:', err);
}
// Create temporary in-memory session
req._tempSession = req._tempSession || {};
// Add temporary session methods
req.session = {
...req._tempSession,
regenerate: (cb) => {
req._tempSession = {};
if (cb) cb();
},
destroy: (cb) => {
req._tempSession = {};
if (cb) cb();
},
reload: (cb) => {
if (cb) cb();
},
save: (cb) => {
if (cb) cb();
},
touch: (cb) => {
if (cb) cb();
},
// Flag to indicate degraded session
_degraded: true
};
// Generate temporary ID if needed
req.sessionID = req.sessionID || `temp_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
// Add header to indicate degraded session
res.set('X-Session-Degraded', 'true');
// Attempt to reconnect Redis in the background
if (!sessionStoreHealthy && redisClient.connected === false) {
redisClient.quit();
setTimeout(() => {
try {
redisClient.connect();
} catch (e) {
console.error('Redis reconnect failed:', e);
}
}, 5000);
}
}
// Continue to next middleware
next();
});
});
// Middleware to warn users about degraded sessions
app.use((req, res, next) => {
if (req.session && req.session._degraded) {
// For API endpoints
if (req.path.startsWith('/api/')) {
const originalJson = res.json;
res.json = function(body) {
if (body && typeof body === 'object') {
body._warnings = [...(body._warnings || []), 'Session storage is temporarily unavailable. Your changes may not be persisted.'];
}
return originalJson.call(this, body);
};
}
// For regular pages (server-rendered)
else if (req.accepts('html')) {
const originalRender = res.render;
res.render = function(view, options, callback) {
options = options || {};
options.sessionWarning = 'Session storage is temporarily unavailable. Your changes may not be persisted.';
return originalRender.call(this, view, options, callback);
};
}
}
next();
});
Advanced Session Store Deployment Patterns
Multi-Region Session Management
Strategies for global applications with users in multiple regions:
// Multi-region session configuration
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const Redis = require('ioredis');
const geoip = require('geoip-lite');
const app = express();
// Define Redis endpoints for different regions
const redisEndpoints = {
'NA': { // North America
host: process.env.REDIS_NA_HOST,
port: process.env.REDIS_NA_PORT,
password: process.env.REDIS_PASSWORD
},
'EU': { // Europe
host: process.env.REDIS_EU_HOST,
port: process.env.REDIS_EU_PORT,
password: process.env.REDIS_PASSWORD
},
'APAC': { // Asia-Pacific
host: process.env.REDIS_APAC_HOST,
port: process.env.REDIS_APAC_PORT,
password: process.env.REDIS_PASSWORD
}
};
// Create Redis clients for each region
const redisClients = {};
for (const [region, config] of Object.entries(redisEndpoints)) {
redisClients[region] = new Redis({
host: config.host,
port: config.port,
password: config.password,
retryStrategy: (times) => Math.min(times * 100, 3000)
});
redisClients[region].on('error', (err) => {
console.error(`Redis ${region} error:`, err);
});
}
// Create session stores for each region
const sessionStores = {};
for (const [region, client] of Object.entries(redisClients)) {
sessionStores[region] = new RedisStore({
client: client,
prefix: `sess:${region}:`
});
}
// Middleware to select the appropriate session store
app.use((req, res, next) => {
// Get client IP
const ip = req.ip ||
req.connection.remoteAddress ||
(req.headers['x-forwarded-for'] || '').split(',')[0].trim();
// Look up location
const geo = geoip.lookup(ip);
// Default to NA if no location found
let region = 'NA';
if (geo) {
// Map country to region
if (['US', 'CA', 'MX'].includes(geo.country)) {
region = 'NA';
} else if (['GB', 'DE', 'FR', 'IT', 'ES', 'NL', 'BE', 'SE', 'NO', 'DK', 'FI', 'PL'].includes(geo.country)) {
region = 'EU';
} else if (['CN', 'JP', 'KR', 'IN', 'AU', 'NZ', 'SG', 'MY', 'TH', 'VN', 'ID', 'PH'].includes(geo.country)) {
region = 'APAC';
}
}
// Use the appropriate session store
session({
store: sessionStores[region],
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
name: `sid_${region.toLowerCase()}`, // Region-specific cookie name
cookie: {
maxAge: 24 * 60 * 60 * 1000
}
})(req, res, next);
});
Auto-Scaling Session Stores
// AWS Elasticache Auto-scaling example
const AWS = require('aws-sdk');
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const Redis = require('ioredis');
const os = require('os');
const app = express();
// Set up AWS SDK
AWS.config.update({
region: process.env.AWS_REGION || 'us-east-1',
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
});
const elasticache = new AWS.ElastiCache();
const cloudwatch = new AWS.CloudWatch();
// Get ElastiCache configuration
async function getRedisEndpoint() {
try {
const replicationGroupName = process.env.ELASTICACHE_REPLICATION_GROUP;
const params = {
ReplicationGroupId: replicationGroupName
};
const result = await elasticache.describeReplicationGroups(params).promise();
if (result.ReplicationGroups && result.ReplicationGroups.length > 0) {
const group = result.ReplicationGroups[0];
// Get primary endpoint for writes
const primaryEndpoint = group.NodeGroups[0].PrimaryEndpoint;
// Get read endpoints for reads
const readEndpoints = group.NodeGroups[0].ReadEndpoint || [];
return {
primaryEndpoint: {
host: primaryEndpoint.Address,
port: primaryEndpoint.Port
},
readEndpoints: readEndpoints.map(endpoint => ({
host: endpoint.Address,
port: endpoint.Port
}))
};
}
throw new Error('No replication group found');
} catch (error) {
console.error('Error getting ElastiCache endpoints:', error);
// Fallback to environment variables
return {
primaryEndpoint: {
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT
},
readEndpoints: []
};
}
}
// Auto-scale based on metrics
async function checkAndScaleRedis() {
try {
const params = {
MetricName: 'CPUUtilization',
Namespace: 'AWS/ElastiCache',
Dimensions: [
{
Name: 'ReplicationGroupId',
Value: process.env.ELASTICACHE_REPLICATION_GROUP
}
],
StartTime: new Date(Date.now() - 5 * 60 * 1000), // Last 5 minutes
EndTime: new Date(),
Period: 60, // 1-minute data points
Statistics: ['Average']
};
const result = await cloudwatch.getMetricStatistics(params).promise();
if (result.Datapoints && result.Datapoints.length > 0) {
// Sort by timestamp (newest first)
const datapoints = result.Datapoints.sort((a, b) => b.Timestamp - a.Timestamp);
// Get the latest CPU utilization
const latestCPU = datapoints[0].Average;
// Auto-scaling logic
if (latestCPU > 70) {
// High CPU usage, scale up
console.log(`High ElastiCache CPU usage (${latestCPU}%), scaling up`);
const params = {
ReplicationGroupId: process.env.ELASTICACHE_REPLICATION_GROUP,
ApplyImmediately: true,
NodeGroupCount: /* current count + 1 */
};
await elasticache.modifyReplicationGroup(params).promise();
} else if (latestCPU < 20 && datapoints.length >= 5) {
// Check if CPU has been consistently low
const allLow = datapoints.slice(0, 5).every(dp => dp.Average < 20);
if (allLow) {
// Low CPU usage, scale down
console.log(`Low ElastiCache CPU usage (${latestCPU}%), scaling down`);
const params = {
ReplicationGroupId: process.env.ELASTICACHE_REPLICATION_GROUP,
ApplyImmediately: true,
NodeGroupCount: /* current count - 1 */
};
await elasticache.modifyReplicationGroup(params).promise();
}
}
}
} catch (error) {
console.error('Error auto-scaling ElastiCache:', error);
}
}
// Initialize Redis client and session store
async function initializeSessionStore() {
const endpoints = await getRedisEndpoint();
// Create Redis cluster client
const redisClient = new Redis.Cluster([
{
host: endpoints.primaryEndpoint.host,
port: endpoints.primaryEndpoint.port
},
...endpoints.readEndpoints.map(endpoint => ({
host: endpoint.host,
port: endpoint.port
}))
], {
redisOptions: {
password: process.env.REDIS_PASSWORD,
tls: process.env.NODE_ENV === 'production' ? {} : undefined
},
scaleReads: 'slave' // Read from slaves
});
// Create session store
const sessionStore = new RedisStore({ client: redisClient });
// Configure session middleware
app.use(session({
store: sessionStore,
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000
}
}));
// Set up auto-scaling check (every 5 minutes)
if (process.env.NODE_ENV === 'production') {
setInterval(checkAndScaleRedis, 5 * 60 * 1000);
}
return { redisClient, sessionStore };
}
// Initialize the app
(async () => {
try {
await initializeSessionStore();
console.log('Session store initialized');
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
} catch (error) {
console.error('Failed to initialize session store:', error);
process.exit(1);
}
})();
Migrating Between Session Stores
Strategies for safely transitioning between different session stores:
// Session store migration
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const MongoStore = require('connect-mongo');
const redis = require('redis');
const mongoose = require('mongoose');
require('dotenv').config();
const app = express();
// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
}).then(() => {
console.log('Connected to MongoDB');
}).catch(err => {
console.error('MongoDB connection error:', err);
});
// Connect to Redis
const redisClient = redis.createClient({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
password: process.env.REDIS_PASSWORD
});
// Create stores
const oldStore = new RedisStore({ client: redisClient });
const newStore = MongoStore.create({
mongoUrl: process.env.MONGODB_URI,
collectionName: 'sessions'
});
// Dual store approach for migration
class MigrationStore extends session.Store {
constructor(options) {
super();
this.oldStore = options.oldStore;
this.newStore = options.newStore;
this.migrationPercentage = options.migrationPercentage || 0;
this.migrationMode = options.migrationMode || 'read-old-write-both';
}
get(sid, callback) {
// Try the new store first
this.newStore.get(sid, (err, session) => {
if (err) return callback(err);
if (session) return callback(null, session);
// Fall back to old store
this.oldStore.get(sid, (oldErr, oldSession) => {
if (oldErr) return callback(oldErr);
if (!oldSession) return callback(null, null);
// Found in old store, migrate to new store
this.newStore.set(sid, oldSession, (migrateErr) => {
if (migrateErr) {
console.error('Migration error:', migrateErr);
} else {
console.log(`Migrated session ${sid} to new store`);
}
// Return the session regardless of migration result
callback(null, oldSession);
});
});
});
}
set(sid, session, callback) {
const rnd = Math.random() * 100;
if (this.migrationMode === 'read-old-write-both' ||
this.migrationMode === 'read-new-write-both' ||
rnd < this.migrationPercentage) {
// Write to both stores
this.newStore.set(sid, session, (newErr) => {
if (newErr) {
console.error('Error writing to new store:', newErr);
}
this.oldStore.set(sid, session, (oldErr) => {
callback(oldErr);
});
});
} else {
// Write only to old store
this.oldStore.set(sid, session, callback);
}
}
destroy(sid, callback) {
// Delete from both stores
this.newStore.destroy(sid, (newErr) => {
this.oldStore.destroy(sid, (oldErr) => {
callback(oldErr || newErr);
});
});
}
touch(sid, session, callback) {
const rnd = Math.random() * 100;
if (this.migrationMode === 'read-old-write-both' ||
this.migrationMode === 'read-new-write-both' ||
rnd < this.migrationPercentage) {
// Touch both stores
this.newStore.touch(sid, session, (newErr) => {
this.oldStore.touch(sid, session, (oldErr) => {
callback(oldErr || newErr);
});
});
} else {
// Touch only old store
this.oldStore.touch(sid, session, callback);
}
}
}
// Migration phases:
// 1. Read old, write both (migrationPercentage = 0)
// 2. Read old, write both (migrationPercentage = 50)
// 3. Read old, write both (migrationPercentage = 100)
// 4. Read new, write both (migrationMode = 'read-new-write-both')
// 5. Read new, write new (migrationMode = 'read-new-write-new')
// Current migration phase (would be stored in configuration/database)
const currentPhase = process.env.MIGRATION_PHASE || '1';
let migrationStore;
switch (currentPhase) {
case '1':
migrationStore = new MigrationStore({
oldStore,
newStore,
migrationPercentage: 0,
migrationMode: 'read-old-write-both'
});
break;
case '2':
migrationStore = new MigrationStore({
oldStore,
newStore,
migrationPercentage: 50,
migrationMode: 'read-old-write-both'
});
break;
case '3':
migrationStore = new MigrationStore({
oldStore,
newStore,
migrationPercentage: 100,
migrationMode: 'read-old-write-both'
});
break;
case '4':
migrationStore = new MigrationStore({
oldStore,
newStore,
migrationPercentage: 100,
migrationMode: 'read-new-write-both'
});
break;
case '5':
// Migration complete, use only the new store
migrationStore = newStore;
break;
default:
migrationStore = oldStore;
}
// Configure session
app.use(session({
store: migrationStore,
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000
}
}));
// Migration status/control endpoint (admin only)
app.get('/admin/migration-status', (req, res) => {
res.json({
currentPhase,
migrationPercentage: migrationStore.migrationPercentage,
migrationMode: migrationStore.migrationMode
});
});
Migration Process
When migrating between session stores, follow these steps:
- Phase 1: Read from old store, write to both (0% of sessions)
- Phase 2: Read from old store, write to both (50% of sessions)
- Phase 3: Read from old store, write to both (100% of sessions)
- Phase 4: Read from new store, write to both (monitor error rates)
- Phase 5: Switch completely to new store
This gradual approach ensures all sessions are properly migrated with minimal disruption.
Additional Resources
Summary
- Session stores are critical components in session-based authentication systems
- The default MemoryStore is not suitable for production due to memory leaks and scaling issues
- Redis is often the preferred session store for high-performance, production applications
- MongoDB and SQL databases provide good alternatives with different trade-offs
- Custom session stores can be implemented for specialized requirements
- Advanced configurations include encryption, compression, and high availability
- Performance optimization techniques vary by store type but focus on minimizing data size and operation frequency
- Monitoring is essential for detecting performance issues and security threats
- Multi-region and auto-scaling configurations support global, high-traffic applications
- Migrating between stores requires a careful, phased approach to minimize disruption
In our next session, we'll explore cookie security best practices, which are essential for protecting session cookies and maintaining secure authentication.