Skip to main content

Guide

iron-session vs express-session vs cookie-session 2026

Compare iron-session, express-session, and cookie-session for managing user sessions in Node.js. Encrypted cookies, server-side stores, and stateless sessions.

·PkgPulse Team·
0

TL;DR

iron-session is the encrypted cookie session library — stateless, no database needed, encrypted with iron-webcrypto, works with Next.js App Router, Express, and any framework. express-session is the standard server-side session middleware — stores sessions in a backend (Redis, database), sends a session ID cookie, the most widely used. cookie-session is Express's client-side session middleware — stores session data in the cookie itself (signed, not encrypted), simple and stateless. In 2026: iron-session for encrypted stateless sessions (especially Next.js), express-session for server-side sessions with Redis/database, cookie-session for simple signed cookie sessions.

Key Takeaways

  • iron-session: ~500K weekly downloads — encrypted cookies, Next.js + any framework, stateless
  • express-session: ~5M weekly downloads — server-side stores, Redis/DB, session ID in cookie
  • cookie-session: ~500K weekly downloads — signed cookies, client-side, Express middleware
  • Stateless (cookie-based) vs stateful (server-side store) — different trade-offs
  • iron-session encrypts the cookie (data is hidden from client)
  • cookie-session signs but doesn't encrypt (data is visible but tamper-proof)

iron-session

iron-session — encrypted cookie sessions:

Next.js App Router

// lib/session.ts
import { getIronSession, IronSession } from "iron-session"
import { cookies } from "next/headers"

export interface SessionData {
  userId?: string
  email?: string
  isLoggedIn: boolean
}

export async function getSession(): Promise<IronSession<SessionData>> {
  return getIronSession<SessionData>(await cookies(), {
    password: process.env.SESSION_SECRET!,  // At least 32 characters
    cookieName: "pkgpulse_session",
    cookieOptions: {
      secure: process.env.NODE_ENV === "production",
      httpOnly: true,
      sameSite: "lax",
      maxAge: 60 * 60 * 24 * 7,  // 7 days
    },
  })
}

// In a Server Action:
export async function login(formData: FormData) {
  "use server"
  const session = await getSession()
  const user = await authenticateUser(formData)

  session.userId = user.id
  session.email = user.email
  session.isLoggedIn = true
  await session.save()

  redirect("/dashboard")
}

// In a Server Component:
export default async function DashboardPage() {
  const session = await getSession()
  if (!session.isLoggedIn) redirect("/login")

  return <h1>Welcome, {session.email}</h1>
}

// Logout:
export async function logout() {
  "use server"
  const session = await getSession()
  session.destroy()
  redirect("/login")
}

Express / Node.js

import { getIronSession } from "iron-session"
import express from "express"

const app = express()

app.use(async (req, res, next) => {
  req.session = await getIronSession(req, res, {
    password: process.env.SESSION_SECRET!,
    cookieName: "session",
  })
  next()
})

app.post("/login", async (req, res) => {
  const user = await authenticate(req.body)
  req.session.userId = user.id
  req.session.isLoggedIn = true
  await req.session.save()
  res.json({ ok: true })
})

app.get("/me", async (req, res) => {
  if (!req.session.isLoggedIn) {
    return res.status(401).json({ error: "Not logged in" })
  }
  res.json({ userId: req.session.userId })
})

How encryption works

iron-session encryption:

1. Session data: { userId: "123", isLoggedIn: true }
2. Serialized: JSON.stringify → '{"userId":"123","isLoggedIn":true}'
3. Encrypted: iron-webcrypto → "Fe26.2**abc123...encrypted-blob..."
4. Set-Cookie: session=Fe26.2**abc123...; HttpOnly; Secure

The client sees the encrypted blob — cannot read or modify it.
Decryption requires the server's password.

vs cookie-session (signed, not encrypted):
  Cookie: session=eyJ1c2VySWQiOiIxMjMifQ==.signature
  Client CAN read the data (base64), but cannot modify it.

express-session

express-session — server-side sessions:

Basic setup

import express from "express"
import session from "express-session"

const app = express()

app.use(session({
  secret: process.env.SESSION_SECRET!,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === "production",
    httpOnly: true,
    sameSite: "lax",
    maxAge: 7 * 24 * 60 * 60 * 1000,  // 7 days
  },
}))

app.post("/login", async (req, res) => {
  const user = await authenticate(req.body)
  req.session.userId = user.id
  req.session.isLoggedIn = true
  res.json({ ok: true })
})

app.get("/me", (req, res) => {
  if (!req.session.isLoggedIn) {
    return res.status(401).json({ error: "Not logged in" })
  }
  res.json({ userId: req.session.userId })
})

app.post("/logout", (req, res) => {
  req.session.destroy((err) => {
    res.json({ ok: true })
  })
})

With Redis store

import session from "express-session"
import RedisStore from "connect-redis"
import Redis from "ioredis"

const redis = new Redis(process.env.REDIS_URL)

app.use(session({
  store: new RedisStore({ client: redis }),
  secret: process.env.SESSION_SECRET!,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true,
    maxAge: 7 * 24 * 60 * 60 * 1000,
  },
}))

// Sessions stored in Redis:
// Key: "sess:sessionId" → { userId: "123", isLoggedIn: true }
// Cookie: connect.sid=s%3AsessionId.signature

With database store

import session from "express-session"
import pgSession from "connect-pg-simple"

const PGStore = pgSession(session)

app.use(session({
  store: new PGStore({
    conString: process.env.DATABASE_URL,
    tableName: "sessions",
    createTableIfMissing: true,
  }),
  secret: process.env.SESSION_SECRET!,
  resave: false,
  saveUninitialized: false,
}))

Session regeneration

// Regenerate session ID after login (prevent session fixation):
app.post("/login", async (req, res) => {
  const user = await authenticate(req.body)

  req.session.regenerate((err) => {
    req.session.userId = user.id
    req.session.isLoggedIn = true

    req.session.save((err) => {
      res.json({ ok: true })
    })
  })
})

cookie-session — signed cookie sessions:

Basic setup

import express from "express"
import cookieSession from "cookie-session"

const app = express()

app.use(cookieSession({
  name: "session",
  keys: [process.env.SESSION_SECRET!],  // Signing keys (array for rotation)
  maxAge: 7 * 24 * 60 * 60 * 1000,     // 7 days
  secure: process.env.NODE_ENV === "production",
  httpOnly: true,
  sameSite: "lax",
}))

app.post("/login", async (req, res) => {
  const user = await authenticate(req.body)
  req.session.userId = user.id
  req.session.isLoggedIn = true
  res.json({ ok: true })
})

app.get("/me", (req, res) => {
  if (!req.session.isLoggedIn) {
    return res.status(401).json({ error: "Not logged in" })
  }
  res.json({ userId: req.session.userId })
})

Key rotation

app.use(cookieSession({
  name: "session",
  // First key signs, all keys verify:
  keys: [
    process.env.SESSION_KEY_NEW!,   // Current signing key
    process.env.SESSION_KEY_OLD!,   // Previous key (still verifies)
  ],
}))

// When rotating keys:
// 1. Add new key to the front of the array
// 2. Keep old key(s) for verification
// 3. After max-age expires, remove old keys

Limitations

cookie-session:
  ✅ Stateless — no server-side store needed
  ✅ Simple — no Redis/database required
  ✅ Key rotation built-in
  ✅ Express middleware

  ❌ Session data is VISIBLE to client (base64, not encrypted)
  ❌ Cookie size limit (~4KB) limits session data
  ❌ Cannot invalidate individual sessions server-side
  ❌ No session listing (can't see active sessions)
  ❌ Express-only (not framework-agnostic)

Feature Comparison

Featureiron-sessionexpress-sessioncookie-session
StorageCookie (encrypted)Server-side (Redis/DB)Cookie (signed)
Encryption❌ (server-side)❌ (signed only)
Data visibilityHidden from clientHidden (server)Visible to client
Server-side invalidation
Max data size~4KBUnlimited~4KB
Framework supportAny (Next.js, Express)ExpressExpress
Redis/DB needed✅ (recommended)
Session listing
Key rotationN/A
TypeScript✅ (@types)✅ (@types)
Weekly downloads~500K~5M~500K

When to Use Each

Use iron-session if:

  • Building with Next.js (App Router or Pages Router)
  • Want encrypted stateless sessions (no Redis/DB)
  • Need framework-agnostic session management
  • Session data is small (< 4KB) and doesn't need server-side invalidation

Use express-session if:

  • Need server-side session storage (Redis, PostgreSQL)
  • Want to invalidate sessions server-side (force logout)
  • Need to store large session data
  • Building with Express and need session listing

Use cookie-session if:

  • Want the simplest possible session middleware
  • Session data is small and non-sensitive (can be visible)
  • Need key rotation for signing keys
  • Building a simple Express app without Redis

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on iron-session v8.x, express-session v1.x, and cookie-session v2.x.

Session Size Limits and What to Store

All three cookie-based solutions (iron-session and cookie-session) and the session ID in express-session share the same browser cookie size limit: approximately 4KB for the entire cookie value, including the name and any encoding overhead. This is a fundamental constraint that shapes what each library can store.

For iron-session and cookie-session, the 4KB limit applies to the actual session data after serialization. A common mistake is trying to store a full user object with profile data, permissions arrays, and preferences in the session — this quickly exceeds the limit. The recommended practice is storing only the minimum identity data: userId, isLoggedIn, and a role or tier string. Anything else should be fetched from the database on each request using the userId as a key. An encrypted iron-session cookie containing { userId: "usr_abc123", isLoggedIn: true, role: "admin" } fits comfortably within the 4KB limit and remains readable after iron-webcrypto's serialization overhead.

express-session sidesteps this constraint because the cookie only stores a session ID (a short random string). The actual session data lives in your Redis or database store. This means express-session can theoretically store unlimited data per session — shopping carts with hundreds of items, multi-step form state, large user preference objects. Redis stores this in memory, making retrieval fast, but it adds a network round-trip to every authenticated request. For applications that already have a Redis instance (for caching or pub/sub), this overhead is negligible. For applications without Redis, adding it solely for sessions means new infrastructure cost and operational complexity.

Security Trade-offs: Encryption vs. Server-Side Storage

The security model of each library deserves careful consideration beyond the simple "encrypted = more secure" framing. Iron-session's encryption means that if your SESSION_SECRET is compromised, all sessions are compromised — there is no mechanism to invalidate individual cookies issued under that secret (short of rotating the secret and logging everyone out). Express-session's server-side model allows targeted invalidation: calling req.session.destroy() immediately invalidates that specific session by deleting it from Redis, even if the user still has the session ID cookie. This is essential for handling security events like password changes, suspected account compromise, or explicit "logout all devices" functionality.

Cookie-session's signed (but not encrypted) approach means session data is readable by anyone who intercepts the cookie — base64 decoding reveals the JSON payload. This is acceptable when session data is non-sensitive (e.g., storing only a non-secret user ID), but a significant issue if the session contains role information, feature flags, or any data the user should not be able to read directly. The keys array in cookie-session provides key rotation for signing, but it does not add encryption. For applications where HTTPS is reliably enforced (preventing interception) and the session only contains a user ID, this is an acceptable trade-off. For applications with sensitive session payloads, iron-session or express-session with server-side storage are the appropriate choices.

Compare session management and auth tooling on PkgPulse →

When to Use Each

Use iron-session if:

  • You are building a Next.js application and want a simple, edge-compatible session solution
  • You want sessions stored in signed/encrypted cookies without a server-side store
  • You want TypeScript types for your session data with no extra setup
  • You need a stateless session that works across serverless and edge deployments

Use express-session if:

  • You are building an Express or Fastify application with traditional server-side session storage
  • You need to store large session data (carts, preferences) that shouldn't travel in cookies
  • You want session invalidation (ability to kill a session from the server side)
  • You are using a Redis, PostgreSQL, or MongoDB session store

Use cookie-session if:

  • You have a small Express application where cookie-only sessions are sufficient
  • You want a simpler alternative to express-session without a backing store
  • Your session data is minimal (just a user ID) and you don't need server-side invalidation

In 2026, the trend in Next.js and edge deployments is toward cookie-based sessions (iron-session) or JWT-based auth (next-auth, Clerk), avoiding server-side session stores that complicate horizontal scaling. For traditional Express APIs that need server-side invalidation, express-session with Redis remains the standard.

A practical note on session security: cookie-based sessions (iron-session, cookie-session) are vulnerable to CSRF if not combined with CSRF protection middleware. Server-side sessions (express-session) stored in Redis are immune to cookie theft but require proper session ID rotation on privilege escalation.

Session Rotation and Privilege Escalation

Regardless of which library you choose, session fixation is a real attack vector that all session management systems must address. When a user's privilege level changes — logging in, completing two-factor verification, upgrading a plan — the session identifier or cookie content must be regenerated with a new value. With express-session, this means calling req.session.regenerate() before setting the authenticated user's data on the new session. With iron-session, since the encrypted cookie contains the full session data, updating the session and calling req.session.save() effectively issues a new encrypted token, achieving the same effect. Failing to rotate sessions after login is the root cause of session fixation attacks where an attacker pre-plants a known session ID, waits for the victim to authenticate with it, and then uses it themselves.

See also: Express vs NestJS and Express vs Koa, better-sqlite3 vs libsql vs sql.js.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.