TL;DR: Session management is how your app remembers that a user is logged in after they've authenticated. The server creates a session (a temporary record of who you are), gives the browser a cookie containing a session ID, and checks that ID on every request. If sessions aren't set up securely — no expiry, no secure flags, data stored in memory — attackers can hijack them and log in as your users.

Why AI Coders Need to Know This

Here's what happens when you ask AI to "add login" to your app: it builds the whole thing — registration, password hashing, login form, the works. You test it locally. It works. You deploy it. Users can log in.

Then things get weird. Users get logged out randomly. Or worse — they stay logged in forever, even after changing their password. Or the absolute worst case: someone figures out how to steal another user's session and access their account without knowing their password.

All three of those problems are session management failures. And they're incredibly common in AI-generated code because the AI optimizes for "it works locally" — not "it's secure in production."

You don't need to become a security researcher. But you need to understand what sessions are, how they work, and what the three or four critical settings are that separate "works on my laptop" from "safe for real users." That's what this article covers.

Real Scenario

You're building a project management tool with Express.js. Users can log in, create projects, and invite teammates. You type this into your AI coding tool:

Prompt You'd Type

Add session-based authentication to my Express app. Users should:
- Log in with email and password
- Stay logged in across page refreshes
- Be able to log out
- Have their session expire after 24 hours
Use express-session with a Redis store for production.

The AI generates a complete session setup in about 10 seconds. It installs packages, configures middleware, sets cookie options, and wires up login and logout routes. Impressive. But is it safe?

Let's look at what it generated, understand each piece, and then find the mistakes it almost certainly made.

Sessions vs. Tokens: Two Ways to Remember Users

Before we dive into the code, you should know there are two main approaches to keeping users logged in. AI might use either one, and it's helpful to understand the tradeoff.

Cookie Sessions (Server-Side) JWT Tokens (Client-Side)
Where user data lives On the server (Redis, database, memory) Inside the token itself (carried by the browser)
What the browser stores A session ID cookie (random string) The entire token (encoded user data + signature)
Server memory needed Yes — server must store session data No — token is self-contained
Easy to revoke access Yes — delete the session from the server Hard — token is valid until it expires
Scales across servers Needs shared store (Redis) Works anywhere (stateless)
Best for Traditional web apps, admin dashboards APIs, mobile apps, microservices
Security risk Session hijacking, session fixation Token theft, no easy revocation

Neither approach is "better" — they solve different problems. This article focuses on server-side cookie sessions because that's what AI typically generates for Express.js apps, and it's the approach most vibe coders encounter first.

What AI Generated

Here's a cleaned-up version of what your AI coding tool would typically produce. This uses express-session (the standard session middleware for Express) with connect-redis as the session store:

// server.js — Session setup with express-session + Redis
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');

const app = express();
app.use(express.json());

// Create Redis client
const redisClient = createClient({ url: 'redis://localhost:6379' });
redisClient.connect().catch(console.error);

// Configure session middleware
app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,     // Signs the session ID cookie
  resave: false,                           // Don't save unchanged sessions
  saveUninitialized: false,                // Don't create empty sessions
  cookie: {
    secure: true,                          // Only send over HTTPS
    httpOnly: true,                        // JavaScript can't read this cookie
    maxAge: 1000 * 60 * 60 * 24,          // 24 hours in milliseconds
    sameSite: 'lax'                        // CSRF protection
  }
}));

// Login route
app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await findUserByEmail(email);

  if (!user || !await verifyPassword(password, user.passwordHash)) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // Store user info in the session
  req.session.userId = user.id;
  req.session.email = user.email;
  req.session.role = user.role;

  res.json({ message: 'Logged in', user: { email: user.email } });
});

// Logout route
app.post('/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) return res.status(500).json({ error: 'Logout failed' });
    res.clearCookie('connect.sid');       // Remove the session cookie
    res.json({ message: 'Logged out' });
  });
});

// Protected route — check if user is logged in
app.get('/dashboard', (req, res) => {
  if (!req.session.userId) {
    return res.status(401).json({ error: 'Not logged in' });
  }
  res.json({ message: `Welcome back, ${req.session.email}` });
});

app.listen(3000);

This is pretty solid AI output — better than average, honestly. But let's break down every piece so you know what you're looking at.

Understanding Each Part

The Session ID

When a user logs in, express-session generates a long, random string — something like s%3AaBC123xyz.... This is the session ID. It's the only thing stored in the user's browser as a cookie.

Think of it like a coat check ticket. You hand your coat (your user data) to the attendant (the server). They give you a ticket (the session ID). Every time you come back, you show the ticket, and they find your coat. The ticket itself doesn't contain any information about your coat — it's just a reference number.

This is why sessions are considered more secure than JWTs for many use cases: the actual user data never leaves the server. The browser only carries a meaningless random string.

The Session Store

The session store is where the server keeps all active sessions. In the code above, that's Redis — a fast, in-memory database perfect for this job.

Here's what a session looks like in Redis:

// Key: sess:aBC123xyz
{
  "userId": 42,
  "email": "chuck@example.com",
  "role": "admin",
  "cookie": {
    "originalMaxAge": 86400000,
    "expires": "2026-03-20T18:00:00.000Z",
    "secure": true,
    "httpOnly": true,
    "sameSite": "lax"
  }
}

When a request comes in with a session ID cookie, express-session looks up that ID in Redis, loads the session data, and attaches it to req.session. If the ID doesn't exist in Redis (expired, deleted, or forged), the user is treated as not logged in.

Why not just use memory? The default MemoryStore keeps sessions in your Node.js process's RAM. This is the number-one mistake AI makes, and we'll cover it in detail below.

The cookie object in the session config controls how the browser handles the session ID cookie. Each setting is a security layer:

  • secure: true — The cookie is only sent over HTTPS. Without this, someone on the same WiFi network could intercept it in plain text.
  • httpOnly: true — JavaScript running in the browser cannot read this cookie. This protects against cross-site scripting (XSS) attacks where malicious scripts try to steal session IDs.
  • maxAge: 86400000 — The cookie (and session) expires after 24 hours. Without this, the session could last forever.
  • sameSite: 'lax' — The browser won't send this cookie with cross-site requests (like from a phishing email's form), which helps prevent CSRF attacks.

The secure and httpOnly Flags — Why They Matter

These two flags deserve extra attention because they're the ones AI most often gets wrong — and leaving them off is like locking your front door but leaving the key in the lock.

secure: true means the cookie only travels over encrypted (HTTPS) connections. During local development on http://localhost, you'll need to set this to false or your sessions won't work at all. That's fine for development. But if you deploy to production without flipping it to true, anyone on the network between your user and your server can read the session ID in transit.

httpOnly: true means the cookie is invisible to JavaScript. Open your browser's console and type document.cookie — an httpOnly cookie won't appear. This matters because if an attacker manages to inject a script into your page (an XSS attack), they can't steal the session cookie and impersonate the user.

Dev vs. Production Tip: A common pattern is to set secure based on your environment: secure: process.env.NODE_ENV === 'production'. This way cookies work locally over HTTP and require HTTPS in production. AI sometimes hardcodes secure: false and you ship it that way without noticing.

What AI Gets Wrong

AI-generated session code almost always works. The problem is it often works insecurely. Here are the three most common mistakes:

Mistake 1: Using the Default Memory Store in Production

This is the most common one. The AI generates something like:

// ⚠️ No store specified — uses MemoryStore by default
app.use(session({
  secret: 'keyboard cat',
  resave: false,
  saveUninitialized: false
}));

This works perfectly on your laptop. But MemoryStore is explicitly not designed for production. Here's why:

  • Memory leak: Sessions accumulate in RAM and are never cleaned up properly. Your server will eventually crash.
  • Lost on restart: Every time your server restarts (deployment, crash, update), all users get logged out.
  • Single server only: If you scale to two servers behind a load balancer, a user might hit server A (which has their session) on one request and server B (which doesn't) on the next. They'll appear randomly logged in and out.

The fix: Always use an external session store. Redis is the most popular choice. PostgreSQL works too if you're already using it as your database.

Mistake 2: Missing or Hardcoded Security Flags

AI frequently generates one of these patterns:

// ⚠️ No cookie settings at all — insecure defaults
app.use(session({
  secret: 'my-secret',
  store: new RedisStore({ client: redisClient })
}));

// ⚠️ Or worse — explicitly disabling security
app.use(session({
  secret: 'my-secret',
  cookie: { secure: false, httpOnly: false }
}));

Without secure: true, the session cookie is sent over unencrypted HTTP connections. Without httpOnly: true, any JavaScript on the page — including injected malicious scripts — can read the session ID.

And notice secret: 'my-secret'? The session secret is used to cryptographically sign the session ID cookie. If an attacker knows your secret, they can forge valid session IDs. AI loves hardcoding secrets like 'keyboard cat' or 'my-secret' right in the code. This should always come from an environment variable.

Mistake 3: No Session Expiry

If you don't set a maxAge on the cookie, the session cookie becomes a "session cookie" in the browser sense — it disappears when the browser closes. But the actual session data stays in your store forever, or until your server restarts.

Even worse, some AI implementations set an extremely long maxAge or skip expiry entirely:

// ⚠️ Session never expires — user stays logged in forever
cookie: {
  maxAge: 1000 * 60 * 60 * 24 * 365  // ONE YEAR
}

A session that never expires means if someone's session ID is stolen (through a shared computer, a compromised network, or a browser extension), the attacker has access indefinitely. Set a reasonable maxAge — 24 hours for most apps, shorter for sensitive ones.

Quick Security Checklist: After AI generates your session code, verify these four things: (1) External session store — not MemoryStore. (2) secure: true in production. (3) httpOnly: true always. (4) Reasonable maxAge — 24 hours is a good default.

How It All Flows Together

Let's trace what happens when a user logs into your app, visits a protected page, and then logs out:

  1. User submits login form → Browser sends email + password to POST /login
  2. Server verifies credentials → Checks email exists and password hash matches
  3. Server creates session → Generates a random session ID, stores user data in Redis under that ID
  4. Server sends cookie → Response includes a Set-Cookie header with the session ID
  5. User visits /dashboard → Browser automatically includes the session cookie with the request
  6. Server looks up session → Reads the session ID from the cookie, finds the matching record in Redis, attaches it to req.session
  7. User is authenticatedreq.session.userId exists, so they see the dashboard
  8. User logs outreq.session.destroy() deletes the session from Redis, res.clearCookie() removes the cookie from the browser

The key insight: the browser sends the session cookie automatically with every request. You don't need to write any client-side code to make it happen. This is a fundamental feature of how cookies work in browsers.

What to Learn Next

Session management doesn't exist in isolation. Here are the concepts that connect directly to what you just learned:

What Is Authentication? The full picture of login systems — how sessions and tokens fit into the authentication flow. What Is JWT? The token-based alternative to sessions. When to use JWTs, how they work, and why they're popular for APIs. What Are Cookies? Sessions rely on cookies. Understand how they work, their security attributes, and the flags that matter. What Is CSRF? Cross-site request forgery exploits how browsers automatically send cookies — including session cookies.

Frequently Asked Questions

What is the difference between a session and a token?

A session stores user data on the server and gives the browser a session ID cookie to reference it. A token (like a JWT) stores user data inside the token itself, which the browser carries with each request. Sessions are server-side state; tokens are client-side state. Sessions are easier to revoke (just delete the server-side record), while tokens are easier to scale (no server-side storage needed).

Why can't I just use the default memory store in production?

The default MemoryStore keeps sessions in your server's RAM. It leaks memory over time because old sessions aren't cleaned up aggressively, it doesn't survive server restarts (logging out everyone), and it can't be shared across multiple server instances. The express-session docs themselves say "it is not designed for a production environment." Use Redis, PostgreSQL, or another external session store.

What does the httpOnly flag on a cookie do?

The httpOnly flag prevents JavaScript running in the browser from reading the cookie. This means if an attacker injects malicious JavaScript into your page (an XSS attack), they still can't steal the session ID. The cookie is only sent automatically by the browser with HTTP requests — it's invisible to document.cookie in JavaScript.

How long should a session last?

It depends on your app. For most web apps (SaaS tools, dashboards, content sites), 24 hours to 7 days is reasonable. For banking or healthcare apps, 15-30 minutes of inactivity should trigger logout. For admin panels, keep it short. Always set a maxAge on your session cookie — never let sessions last forever. You can also implement "sliding sessions" where the expiry resets on each request, so active users don't get kicked out.

Do I need session management if I'm using JWT tokens?

Not traditional server-side sessions, but you still need to manage the token lifecycle — where it's stored (never localStorage for sensitive apps), when it expires, and how to revoke access if needed. Many production apps use a hybrid: short-lived JWTs (15 minutes) for API calls plus a server-side refresh token that functions like a session. So even in "token-based" architectures, session concepts show up.