User Session Management — Low-Level Design
A session management system authenticates users and maintains their login state across requests. It must handle session creation, validation, expiry, revocation, and multi-device management. This is asked at nearly every company — it underpins every authenticated product.
Core Data Model
Session
id TEXT PK -- random 32-byte hex token
user_id BIGINT NOT NULL
created_at TIMESTAMPTZ NOT NULL
expires_at TIMESTAMPTZ NOT NULL
last_active_at TIMESTAMPTZ NOT NULL
ip_address INET
user_agent TEXT
device_id TEXT -- stable per-device identifier
revoked_at TIMESTAMPTZ -- null = active
-- Index for fast per-user session listing (account management page)
CREATE INDEX idx_sessions_user ON Session(user_id, created_at DESC)
WHERE revoked_at IS NULL;
Session Creation on Login
import secrets
def create_session(user_id, ip_address, user_agent):
# Cryptographically secure random token — not a UUID
token = secrets.token_hex(32) # 64-character hex string, 256 bits of entropy
db.execute("""
INSERT INTO Session
(id, user_id, created_at, expires_at, last_active_at, ip_address, user_agent)
VALUES
(%(token)s, %(uid)s, NOW(), NOW() + INTERVAL '30 days',
NOW(), %(ip)s, %(ua)s)
""", {'token': token, 'uid': user_id, 'ip': ip_address, 'ua': user_agent})
# Cache in Redis for fast validation (avoid DB hit on every request)
redis.setex(f'session:{token}', 3600, json.dumps({
'user_id': user_id,
'expires_at': (now() + timedelta(days=30)).isoformat()
}))
return token
Session Validation on Each Request
def validate_session(token):
# 1. Check Redis cache first
cached = redis.get(f'session:{token}')
if cached:
data = json.loads(cached)
if data['expires_at'] > now().isoformat():
# Extend Redis TTL on activity (sliding window)
redis.expire(f'session:{token}', 3600)
return data['user_id']
# 2. Cache miss or expired — check DB
session = db.execute("""
SELECT user_id, expires_at, revoked_at
FROM Session
WHERE id = %(token)s
""", {'token': token}).first()
if not session:
raise InvalidSession('Session not found')
if session.revoked_at is not None:
raise InvalidSession('Session revoked')
if session.expires_at < now():
raise InvalidSession('Session expired')
# Refresh cache
redis.setex(f'session:{token}', 3600, json.dumps({
'user_id': session.user_id,
'expires_at': session.expires_at.isoformat()
}))
# Update last_active_at asynchronously (avoid write on every request)
update_last_active_async(token)
return session.user_id
Session Revocation
def revoke_session(token):
"""Logout: revoke a single session."""
db.execute("""
UPDATE Session SET revoked_at=NOW()
WHERE id=%(token)s AND revoked_at IS NULL
""", {'token': token})
redis.delete(f'session:{token}') # Immediate cache invalidation
def revoke_all_sessions(user_id, except_token=None):
"""Logout all devices."""
db.execute("""
UPDATE Session SET revoked_at=NOW()
WHERE user_id=%(uid)s
AND revoked_at IS NULL
AND (%(except)s IS NULL OR id != %(except)s)
""", {'uid': user_id, 'except': except_token})
# Cannot efficiently invalidate all user's Redis keys without a user→sessions index
# Option 1: store session IDs in a Redis set per user
session_ids = redis.smembers(f'user_sessions:{user_id}')
for sid in session_ids:
redis.delete(f'session:{sid}')
redis.delete(f'user_sessions:{user_id}')
JWT vs Opaque Tokens
Opaque tokens (recommended for most applications):
+ Revocation is immediate: delete from Redis/DB
+ No sensitive data in the token
+ Token size: 64 chars
- Requires server-side lookup on every request
JWT (stateless tokens):
+ No server-side storage needed; validated by signature alone
+ Works well for microservices (no shared session store)
- Revocation is hard: token is valid until expiry unless you maintain a denylist
(which defeats the stateless benefit)
- Access token expiry must be short (15 min) → requires refresh token flow
- Sensitive data can be read if not encrypted (JWE vs JWS)
Use JWT for: short-lived API access tokens, service-to-service auth
Use opaque tokens for: user sessions where logout must be instant
Key Interview Points
- Use secrets.token_hex(32), not UUID: UUID v4 has only 122 bits of entropy and is not designed for security tokens. A 32-byte random hex string from a CSPRNG has 256 bits — brute-force infeasible.
- Redis TTL shorter than DB expiry: Redis TTL of 1 hour with DB expiry of 30 days provides fast validation on active sessions while ensuring inactive sessions expire from the cache automatically.
- Async last_active_at update: Writing last_active_at on every authenticated request doubles your write load. Buffer updates and flush every 60 seconds, or use a sliding window with Redis EXPIRE.
- Revocation requires cache invalidation: DELETE from Redis on logout. Without this, a revoked session remains valid in the cache for up to 1 hour — unacceptable for security-sensitive logout scenarios.
{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”Why use a random token instead of a UUID for session IDs?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”UUIDs (v4) provide 122 bits of randomness and are designed to be unique, not secret. A session token must be both unique and unpredictable — it acts as a credential. secrets.token_hex(32) generates 256 bits from a Cryptographically Secure Pseudo-Random Number Generator (CSPRNG), producing tokens that are computationally infeasible to brute-force. UUID v1 uses the MAC address and timestamp, making it partially predictable. Always use a CSPRNG for security tokens; UUID is for identifiers, not secrets.”}},{“@type”:”Question”,”name”:”How does Redis caching reduce database load for session validation?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Every authenticated API request must validate the session token. Without caching: every request hits the database. A service handling 10,000 requests/second generates 10,000 session lookups/second — a significant DB load for a simple SELECT by primary key. With Redis caching: the session is stored in Redis with a 1-hour TTL. The first request after login caches the session. Subsequent requests within 1 hour hit Redis (~0.1ms) instead of the DB (~5ms). The Redis TTL slides forward on each request (via EXPIRE) to keep active sessions warm.”}},{“@type”:”Question”,”name”:”What is the correct way to implement logout?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Three actions on logout: (1) DELETE the session token from the database (or UPDATE revoked_at=NOW()). (2) DELETE the session key from Redis to invalidate the cache immediately — without this, the token remains valid in Redis for up to 1 hour after logout. (3) Clear the session cookie on the client (Set-Cookie with max-age=0 or expired date). All three are required. Skipping the Redis invalidation means a stolen token (from a compromised device) can be used for up to 1 hour after the user logs out.”}},{“@type”:”Question”,”name”:”What are the tradeoffs between JWT and opaque session tokens?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Opaque tokens: require a server-side store (DB + Redis) for validation; revocation is immediate (delete the record); every request hits the session store; no sensitive data in the token. JWT (stateless): validated by cryptographic signature without a session store; revocation requires a denylist (which negates the stateless benefit); access tokens must have short expiry (15 min) compensated by refresh tokens; claims are readable without decryption (use JWE for sensitive data). Use opaque tokens for user sessions needing immediate logout; use JWT for service-to-service auth or short-lived access tokens.”}},{“@type”:”Question”,”name”:”How do you implement "remember me" (persistent login)?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Create two session types: a short-lived session (24 hours, standard) and a long-lived session (30 days, "remember me"). For long-lived sessions: store session in a persistent cookie (with Max-Age or Expires attribute) rather than a session cookie (which expires when the browser closes). Set the cookie’s HttpOnly, Secure, and SameSite=Strict attributes. Refresh the session expiry on each use (sliding window): UPDATE Session SET expires_at=NOW()+30days whenever the token is validated. The database record and cookie both extend together.”}}]}
User session management and authentication design is discussed in Google system design interview questions.
Session management and login system design is covered in Meta system design interview preparation.
User session and multi-device authentication design is discussed in Netflix system design interview guide.