Omit the store option and express-session 6,354 uses its built-in MemoryStore, which warns on startup for two good reasons: it never evicts expired sessions, so it leaks, and it lives inside one process, so every restart logs everyone out and a second worker (Clustering) sees none of the first worker's sessions. A store is a tiny interface — get, set, destroy, optionally touch — so there are dozens of them. Redis 2,763 is the usual answer.
import { createClient } from 'redis'; // npm i redis connect-redis@10.0.0
import { RedisStore } from 'connect-redis';
const client = createClient({ url: process.env.REDIS_URL });
await client.connect();
app.use(session({
store: new RedisStore({ client, prefix: 'sess:', ttl: 1800 }),
name: '__Host-sid',
secret: [process.env.SESSION_SECRET, process.env.SESSION_SECRET_OLD].filter(Boolean),
resave: false, saveUninitialized: false, rolling: true,
cookie: { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 1800000 }
}));Redis EXPIRE does the cleanup, so ttl should match the cookie's maxAge; with rolling: true the store's touch pushes the TTL out on each request. An array as secret rotates signing keys without logging anyone out: new cookies use the first entry, old ones still verify against the second.
| Store | Package | Fits | Watch out for |
|---|---|---|---|
| Redis | connect-redis 10.0.0 2,823 | most APIs | another service to run |
| MongoDB 1,815 | connect-mongo 6.0.0 1,968 | you already run Mongo | TTL index does the reaping |
| Files | session-file-store 1.5.0 | one small VM | no sharing between hosts |
| Memory + LRU | memorystore 1.6.8 | dev, single process | lost on restart |
Configure two timeouts, not one. maxAge with rolling: true is an idle timeout; an absolute one needs your own check, because a session touched every minute never expires. Record req.session.createdAt at login and destroy the session once its age passes policy.