iron-session vs express-session vs cookie-session: Session Management in Node.js (2026)
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
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
| Feature | iron-session | express-session | cookie-session |
|---|---|---|---|
| Storage | Cookie (encrypted) | Server-side (Redis/DB) | Cookie (signed) |
| Encryption | ✅ | ❌ (server-side) | ❌ (signed only) |
| Data visibility | Hidden from client | Hidden (server) | Visible to client |
| Server-side invalidation | ❌ | ✅ | ❌ |
| Max data size | ~4KB | Unlimited | ~4KB |
| Framework support | Any (Next.js, Express) | Express | Express |
| Redis/DB needed | ❌ | ✅ (recommended) | ❌ |
| Session listing | ❌ | ✅ | ❌ |
| Key rotation | ❌ | N/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.