Express sessions are stored in memory by default, which is fine for a single-server development setup. But as soon as you need to scale out, or even just restart your server, all your user session data vanishes. Using Redis as a session store fixes this by giving you a centralized, persistent place to keep session data that’s accessible from any server instance.
Let’s see it in action. Imagine a simple Express app with Redis-backed sessions.
const express = require('express');
const session = require('express-session');
const redis = require('redis');
const RedisStore = require('connect-redis')(session);
const app = express();
// Configure Redis client
const redisClient = redis.createClient({
url: 'redis://localhost:6379' // Your Redis server address
});
redisClient.on('connect', () => {
console.log('Connected to Redis');
});
redisClient.on('error', (err) => {
console.error('Redis connection error:', err);
});
// Initialize Redis store
const redisStore = new RedisStore({
client: redisClient,
prefix: 'sess:', // Optional: prefix for session keys in Redis
});
// Configure session middleware
app.use(session({
store: redisStore,
secret: 'your-super-secret-key', // Change this to a strong, random string
resave: false, // Don't save session if unmodified
saveUninitialized: false, // Don't create session until something is stored
cookie: {
secure: process.env.NODE_ENV === 'production', // Use secure cookies in production
httpOnly: true, // Prevent client-side JS from accessing cookies
maxAge: 1000 * 60 * 60 * 24 // 24 hours
}
}));
// Example route to set a session variable
app.get('/login', (req, res) => {
req.session.user = { id: 123, username: 'alice' };
res.send('Logged in!');
});
// Example route to get a session variable
app.get('/profile', (req, res) => {
if (req.session.user) {
res.send(`Welcome, ${req.session.user.username}!`);
} else {
res.send('Not logged in.');
}
});
// Example route to destroy a session
app.get('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
console.error('Session destruction error:', err);
return res.status(500).send('Could not log out.');
}
res.send('Logged out!');
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
When a user visits /login, req.session.user is set. This data is then serialized and sent to Redis under a key like sess:some_session_id. The some_session_id is stored in a cookie on the user’s browser. On subsequent requests to /profile, Express reads the cookie, fetches the session data from Redis using that ID, and deserializes it back into req.session.user. If the user logs out, req.session.destroy() tells Redis to remove the session key.
The core problem this solves is state management in a distributed or ephemeral environment. In-memory sessions tie your session state to a specific server process. If that process dies, restarts, or if you add more servers behind a load balancer, users will be logged out inconsistently or entirely. Redis acts as a shared, durable datastore for this session state, making your application state resilient to individual server failures and enabling horizontal scaling.
You control the session’s lifecycle and its contents. secret is crucial for signing session IDs to prevent tampering; it must be kept private and be a strong, unpredictable string. resave: false and saveUninitialized: false are common optimizations to avoid unnecessary writes to Redis and prevent the creation of sessions for unauthenticated users. The cookie object dictates how the session ID is managed by the browser, with maxAge determining its expiration.
connect-redis uses redis-commands under the hood to interact with your Redis instance. When req.session.user = ... is assigned, Express’s session middleware, in conjunction with connect-redis, serializes the session object (typically to JSON) and issues a SET command to Redis. The key will be sess:${sessionID}, and the value will be the serialized session data. The sessionID itself is stored in the cookie. When a request comes in with a session cookie, the middleware reads the cookie, retrieves the sessionID, and executes a GET command on Redis with that key. If found, the data is deserialized and attached to req.session. For req.session.destroy(), it executes a DEL command.
A common point of confusion arises with maxAge and expires. While maxAge (in milliseconds) is the preferred way to set session duration, older versions of express-session or certain configurations might default to using expires. If you set cookie.expires = new Date(Date.now() + 1000 * 60 * 60 * 24), this cookie will be sent to the browser with an explicit expiration date. However, Redis itself also has its own TTL (Time To Live) for keys. If you set cookie.maxAge, connect-redis will typically also set a TTL on the Redis key using EX or PEX options within the SET command. When both are present, the shorter of the two durations will effectively govern when the session becomes inaccessible. The browser cookie expiring is one thing, but the session data disappearing from Redis is what truly ends the session server-side.
You’ll next want to consider implementing session fingerprinting to mitigate session hijacking.