Skip to content

Session secret and store

Severity: criticalApplies to: express-session 1.xFacts last verified 2026-07-26 against express-session 1.19.0

Two defaults to change here, and both are things you inherit by following the package’s own quick-start rather than things you configure wrongly.

express-session’s documentation is blunt about its default:

Warning The default server-side session storage, MemoryStore, is purposely not designed for a production environment. It will leak memory under most conditions, does not scale past a single process, and is meant for debugging and developing.

“Purposely” is the word to notice. This is not a limitation to work around — the default exists to make the quick-start work, and shipping it is the mistake.

The security consequence is not just memory. A store that does not scale past one process means sessions break the moment you run two instances, and the usual field fix is sticky sessions or a shorter session lifetime, neither of which is a security decision anyone made deliberately.

secret signs the session ID cookie. The README’s example value is 'keyboard cat', and it appears verbatim in a great many deployed applications because it is what you get by copying the working example.

the fix
import session from "express-session";
import { RedisStore } from "connect-redis";
app.use(
session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET, // throw at boot if unset
resave: false,
saveUninitialized: false,
cookie: { secure: true, httpOnly: true, sameSite: "lax" },
})
);

saveUninitialized: false is worth setting deliberately: the default true writes a session record for every visitor who has not logged in, which on a public site means your store fills with empty sessions from crawlers.

verify it workedRun this in: shell
Terminal window
node -e "process.env.SESSION_SECRET || (console.error('SESSION_SECRET unset'), process.exit(1))"

Better still, verify at boot. Reading process.env.SESSION_SECRET and throwing when it is missing turns a silent weak-secret deploy into a failed one.

if you see this
Warning: connect.session() MemoryStore is not designed for a production environment, as it will leak memory, and will not scale past a single process.

express-session is running with no store configured. Sessions live in the process, so they vanish on restart and are not shared between instances.

Read in: express-session — emitted at startup when no store option is given

The MemoryStore warning goes to stderr at startup, which is exactly where it gets lost in container logs. It is worth grepping for on a deploy you did not set up yourself.

before you ship this

Rotating secret invalidates every existing session at once — everyone is logged out. That is correct if the secret leaked, and disruptive if you are merely tidying up, so pick your moment. express-session accepts an array of secrets, where the first signs and the rest still verify, which is the equivalent of Django’s SECRET_KEY_FALLBACKS and lets you rotate without the mass logout.

Moving off MemoryStore means sessions now depend on an external service being reachable. That is a real availability trade — a Redis outage becomes a login outage — and it is the right trade, but it needs the store treated as a production dependency rather than a cache.

  • Session cookie flags — the other half of session security
  • If your session store is now a Redis you just stood up, it needs its own authentication and network binding — an unauthenticated session store holds every session in the application.