If you’re using default Express sessions, your users are getting kicked out every single time you deploy a code change, restart PM2, or spin up a second server instance. That’s because Express defaults to storing sessions in local memory. I hit this exact wall on a production app when users kept complaining about random logouts during minor middleware updates. To fix this, you need a fast, external, persistent store. Let’s set up Express sessions with Redis using modern Node.js packages so you never have to deal with this headache again.

Why the Default Memory Store is a Production Disaster
Express ships with an in-memory session store (MemoryStore). It’s fine for a quick local test, but it’s a disaster for production. Because it keeps everything inside the running Node.js process, you run into three massive walls:
- Memory Leaks: It doesn’t clean up expired sessions properly. Over time, your memory footprint creeps up until your container eventually crashes with an out-of-memory error.
- Zero Persistence: Restart your Node app, and the memory heap is wiped clean. Every single active user session vanishes instantly.
- No Horizontal Scaling: The moment you scale to two or more servers behind a load balancer, server A has no idea about the session created on server B. Your users will get random 401s as their requests bounce between servers.
By moving session data to Redis, we decouple session state from application state. If your Node app crashes, restarts, or scales up, the sessions stay safe and sound in Redis. This is the exact same pattern we use when building a Redis-backed idempotency middleware for Express.
Spinning Up a Local Redis Instance
Before writing any code, we need a running Redis instance. I highly recommend using Docker for this—it’s the cleanest way to get Redis up and running locally without cluttering your host OS. Run this command to spin up a lightweight Redis container:
docker run -d --name express-redis-store -p 6379:6379 redis:7-alpine
This pulls the Alpine-based Redis image and maps port 6379 to your local machine. To verify everything is working, ping the container:
docker exec -it express-redis-store redis-cli ping
If you get a PONG back, your Redis instance is ready to roll.
Installing the Dependencies
Let’s initialize a fresh Node.js project and install what we need. We’ll use the official node-redis client, the standard express-session package, and its Redis connector. Run these commands in your terminal:
mkdir express-redis-sessions
cd express-redis-sessions
npm init -y
npm install express express-session redis connect-redis dotenv
Note that we’re using connect-redis version 7.x. Older versions forced you to pass the session object directly into the initializer function, which was always a bit clunky. The new version works with the modern v4 Node Redis client.
Configuring Express Sessions with Redis
Create a server.js file in your root directory. We need to initialize the Redis client, set up the connect-redis store, and hook it into our express-session middleware. Here’s how to wire it up:
const express = require('express');
const session = require('express-session');
const { createClient } = require('redis');
const RedisStore = require('connect-redis').default;
require('dotenv').config(); const app = express();
const PORT = process.env.PORT || 3000; // 1. Initialize Redis Client
const redisClient = createClient({ url: process.env.REDIS_URL || 'redis://localhost:6379'
}); redisClient.connect().catch(err => { console.error('Could not establish a connection with Redis:', err);
}); // 2. Initialize Redis Store
const redisStore = new RedisStore({ client: redisClient, prefix: "sess:"
}); // 3. Configure Express Session Middleware
app.use(session({ store: redisStore, secret: process.env.SESSION_SECRET || 'super-secret-development-key', resave: false, saveUninitialized: false, cookie: { secure: false, // Set to true if running over HTTPS httpOnly: true, maxAge: 1000 * 60 * 60 * 24 // 24 hours }
})); app.get('/', (req, res) => { res.send('Session server is running.');
}); app.listen(PORT, () => { console.log(`Application is listening on port ${PORT}`);
});
A couple of quick details here: we set resave: false so we aren’t constantly hammering Redis with writes for unchanged sessions. We also set saveUninitialized: false to avoid creating empty sessions for users who aren’t even logged in. This keeps your Redis memory footprint nice and clean.
Testing and Verifying Session Persistence
Let’s build some quick test routes to make sure sessions are actually landing in Redis. We’ll set up a login route to write session data, a profile route to read it, and a logout route to destroy it. Drop these routes into server.js right above your app.listen call:
app.get('/login', (req, res) => { req.session.user = { id: 'usr_98472', username: 'fahim_dev', role: 'admin' }; res.send('You are logged in. Visit /profile to see your details.');
}); app.get('/profile', (req, res) => { if (!req.session.user) { return res.status(401).send('Unauthorized. Please login first.'); } res.json({ message: 'Welcome back!', user: req.session.user });
}); app.get('/logout', (req, res) => { req.session.destroy(err => { if (err) { return res.status(500).send('Could not log out.'); } res.clearCookie('connect.sid'); res.send('Logged out successfully.'); });
});
Fire up your server with node server.js and let’s test the flow:
- Hit
http://localhost:3000/loginin your browser. You should see a success message. - Now go to
http://localhost:3000/profile. You’ll see your session data printed out as JSON. - Let’s inspect Redis directly to confirm the session is actually in there.
Run this command inside your terminal to check for active keys:
docker exec -it express-redis-store redis-cli KEYS "sess:*"
You should see a key pop up like sess:your-session-id-here. This is proof that your session state is completely decoupled from your Node.js process. Go ahead and stop your Node server, restart it, and refresh the profile page—you’re still logged in!
Handling Redis Outages Gracefully
What happens if your Redis instance goes down? By default, if the Redis client loses its connection, your Express app will either hang on requests or crash with unhandled exceptions. This is where hobby projects break and production apps survive. We need to handle connection events and set up an automatic retry strategy. Let’s update our Redis client initialization to handle reconnects gracefully:
const redisClient = createClient({ url: process.env.REDIS_URL || 'redis://localhost:6379', socket: { reconnectStrategy: (retries) => { if (retries > 10) { console.error('Redis reconnection failed. Max retries reached.'); return new Error('Redis connection lost'); } // Exponential backoff up to 3 seconds return Math.min(retries * 100, 3000); } }
}); redisClient.on('error', (err) => console.error('Redis Client Error:', err));
redisClient.on('connect', () => console.log('Redis client connected successfully'));
With this retry strategy, if your Redis server drops for a few seconds during a maintenance window or a brief network hiccup, the client will keep trying to reconnect in the background without bringing down your entire web server.
Security and Production Checklist
Before you push this to production, you need to lock down your cookies and connection security. Here’s my quick checklist of things you must change before going live:
- Secure Cookies: Set
cookie.secure: truein your session configuration so cookies are only sent over HTTPS. If you’re behind a reverse proxy (like Nginx, AWS ALB, or Cloudflare), you’ll also need to addapp.set('trust proxy', 1)to your Express app, or the cookies won’t get set. - Use a Strong Secret: Don’t use a hardcoded development secret. Pull a high-entropy string from your environment variables (
process.env.SESSION_SECRET). - Restrict Redis Access: Never expose your Redis port (6379) to the public internet. Keep it inside your private VPC or at least secure it with strong password authentication.
If you want to build more advanced Redis integrations, check out how to cache API responses with Redis in Node.js or implement a Redis sliding window rate limiter in Node.js to keep your auth routes safe from brute-force attacks.
Frequently Asked Questions
Does connect-redis automatically delete expired sessions from Redis?
Yes. It automatically sets a TTL (Time To Live) on your Redis keys based on your session’s cookie.maxAge setting. Once that time runs out, Redis handles the cleanup automatically.
Can I share sessions across different subdomains?
Yes. You just need to configure the cookie.domain property. For example, setting domain: '.yourdomain.com' allows both app.yourdomain.com and api.yourdomain.com to share the same session cookie.
Should I use Redis for all application caching?
Redis is incredibly versatile and great for caching database queries, rate limiting, and temporary state. If you end up running multiple app instances and need to handle complex race conditions, check out our guide on building a Redis distributed lock in Node.js.
Next Steps
Now that your session state is decoupled from your application process, you can scale your Node.js app horizontally behind a load balancer without kicking off your users. To keep optimizing your stack, take a look at our guide on how to Cache API Responses with Redis in Node.js.

