Sessions are the mechanism web applications use to maintain state across stateless HTTP requests. Every authenticated request needs to verify who the user is and what they are allowed to do. Where that session data lives affects every request’s latency.

Why not store sessions in a relational database

A relational database can store sessions. Many applications do. The table looks like:

CREATE TABLE sessions (
  id         TEXT PRIMARY KEY,
  user_id    INTEGER REFERENCES users(id),
  data       JSONB,
  expires_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT now()
);

This works. The problem is that session reads happen on every authenticated request. For a high-traffic application, sessions might represent 30-50% of database reads. These reads hit a table with high write churn (sessions created and destroyed constantly), require an index lookup, and return small rows.

Relational databases are optimized for complex queries, joins, transactions, and durability of business data. They are over-engineered for “look up a session ID and return a JSON blob.”

Redis is a key-value store that lives entirely in memory. A session lookup is a single key read against memory. It is 10-100x faster than a database query and adds no disk I/O.

Setting up Redis sessions

Node.js with Express and connect-redis:

import session from 'express-session';
import { createClient } from 'redis';
import { RedisStore } from 'connect-redis';

const redisClient = createClient({
  url: process.env.REDIS_URL,
});
await redisClient.connect();

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000, // 24 hours
  },
}));

Python with Flask and Flask-Session:

from flask import Flask, session
from flask_session import Session
import redis

app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ['SECRET_KEY']
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_REDIS'] = redis.from_url(os.environ['REDIS_URL'])
app.config['SESSION_PERMANENT'] = False
app.config['SESSION_USE_SIGNER'] = True

Session(app)

@app.route('/login', methods=['POST'])
def login():
    # Validate credentials...
    session['user_id'] = user.id
    session['email'] = user.email
    return redirect('/')

Session data structure in Redis

Each session is stored as a key with a TTL:

Key:   session:abc123def456
Value: {"user_id": 42, "email": "user@example.com", "cart": [...]}
TTL:   86400 seconds

Redis handles expiry automatically. When the TTL expires, the key is deleted. No cleanup cron job needed.

What to store in sessions

Sessions should be small. The session is loaded on every authenticated request. Loading 10KB per request adds up at scale.

Store identifiers and flags, not full objects:

// Good: store minimal data
session.user_id = 42;
session.is_admin = false;

// Bad: store entire objects
session.user = { id: 42, name: 'Alice', email: '...', preferences: {...}, billing: {...} };

Fetch the full user object from a cached database query when needed, using user_id as the lookup key.

Redis persistence and session loss

By default Redis is an in-memory store. A Redis restart loses all data. For sessions this is acceptable: users get logged out, they log back in. Annoying but not catastrophic.

If you want sessions to survive Redis restarts, enable AOF (Append Only File) persistence:

# redis.conf
appendonly yes
appendfsync everysec

This writes every command to disk. Sessions survive restarts but Redis writes become slightly slower due to disk I/O.

An alternative is replication: run a Redis replica that takes over if the primary fails. Failover happens in seconds rather than minutes of data recovery.

Horizontal scaling consideration

Sessions in Redis enable stateless application servers. Any server in your fleet can serve any request because session data lives in Redis, not in application server memory. This is a prerequisite for horizontal scaling.

The alternative - sticky sessions - pins each user to a specific server. This creates uneven load distribution and makes server restarts disruptive. Redis sessions eliminate the need for stickiness entirely.

Security considerations

The session cookie contains only an opaque session ID. The actual data is in Redis. If the cookie is stolen, the attacker can use it until expiry, but they cannot read the session data from the cookie itself.

Set the httpOnly flag on the cookie to prevent JavaScript from reading it. Set secure in production so the cookie is only sent over HTTPS. Set sameSite: 'lax' or 'strict' to reduce CSRF exposure.

Rotate the session ID on privilege escalation - log in, change password, gain admin access. A rotated session ID prevents session fixation attacks where an attacker plants a known session ID before login.

Redis for session storage is a simple architectural decision with clear benefits: lower latency, no database load from session reads, and stateless application servers that scale horizontally.