Skip to main content

iron-session vs express-session vs cookie-session: Session Management in Node.js (2026)

·PkgPulse Team

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.

Compare session management and auth tooling on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.