Skip to main content

How to Store User Sessions in Redis

Web applications need to remember who you are between requests. After you log in, the server creates a session containing your user ID, permissions, and other state. Each subsequent request includes a session ID that the server uses to look up this data. The challenge is where to store sessions when you have multiple application servers behind a load balancer.

Storing sessions in memory on each app server requires sticky sessions, where the load balancer routes each user to the same server. This complicates scaling and failover. Storing sessions in a relational database works but adds latency to every request. Redis solves both problems: it is fast enough for per-request lookups, shared across all app servers, and supports automatic expiration for session cleanup. This guide covers three approaches: simple serialized sessions with strings, field-based sessions with hashes, and keeping sessions alive with sliding expiration.

Which Redis data types we will use

String is used in two ways in this implementation:

  1. As a serialized session store where all session data (usually JSON) is stored as a single string value. The key is the session ID, which should be cryptographically random. One GET retrieves the session, one SET with expiration creates or updates it.
  2. With TTL for automatic expiration. When creating a session, set an expiration time with SET key value EX seconds. Redis automatically deletes expired sessions. The TTL can be fixed (session dies after 30 minutes) or sliding (refreshed on each request).

Hash stores session data as individual fields instead of serializing everything into one string. This lets different parts of your application update different fields without reading and rewriting the entire session. The shopping cart module can increment cart_items while the auth module updates last_activity, and you can inspect session contents directly in Redis.

The choice between string and hash depends on how you access session data. If you always read/write the entire session at once, strings are simpler. If you update individual fields frequently, hashes are more efficient.

Storing sessions as serialized strings

The simplest session store serializes all session data into a single string. The key is the session ID, and the value is JSON or another serialization format containing the session data. Set a TTL when creating the session, and Redis automatically deletes it when it expires.

This approach works well when session data is small and you always read or write the entire session at once. Most web frameworks treat sessions this way. The session ID is typically a cryptographically random string stored in a cookie. On each request, the server fetches the session by ID, deserializes it, and makes it available to the application.

The downside is that you cannot update individual fields without reading and rewriting the entire session. For sessions with many fields or frequent partial updates, this adds overhead. You also lose visibility into session contents from Redis tools since the data is opaque serialized bytes.

# Create a session with 30 minute expiration
# Session ID should be cryptographically random
SET session:abc123def456 '{"user_id":"user789","role":"admin","cart_items":3}' EX 1800
> OK

# Retrieve the session on subsequent requests
GET session:abc123def456
> '{"user_id":"user789","role":"admin","cart_items":3}'

# Check if a session exists without fetching data
EXISTS session:abc123def456
> 1

# Delete session on logout
DEL session:abc123def456
> 1

# Check remaining TTL for debugging
TTL session:abc123def456
> 1423 (seconds remaining)

Storing sessions as structured fields

When you need to read or update individual session fields without touching the entire session, Redis hashes are a better fit. Each session is a hash where fields map to values. You can get or set specific fields with HGET and HSET, or fetch everything with HGETALL.

Hashes work well for sessions with many fields or when different parts of your application update different fields. A shopping cart module updates cart_items while an authentication module updates last_activity. Neither needs to serialize and deserialize the entire session. You can also inspect session contents directly in Redis using HGETALL, which helps with debugging.

The tradeoff is that hash values are strings, so nested objects still require serialization. If your session contains a complex preferences object, you either serialize it into one hash field or flatten it into multiple fields. Hashes also require explicit expiration management since HSET does not accept TTL directly.

# Create a session as a hash
HSET session:abc123 user_id "user789" role "admin" cart_items "3" created_at "1699900000"
> 4 (fields set)

# Set expiration on the hash key
EXPIRE session:abc123 1800
> 1

# Get a single field
HGET session:abc123 user_id
> "user789"

# Get multiple fields
HMGET session:abc123 user_id role
> ["user789", "admin"]

# Update one field without affecting others
HSET session:abc123 cart_items "5"
> 0 (field updated, not created)

# Get all session data
HGETALL session:abc123
> {"user_id": "user789", "role": "admin", "cart_items": "5", "created_at": "1699900000"}

# Increment a numeric field
HINCRBY session:abc123 cart_items 1
> 6

Keeping active sessions alive with sliding expiration

Fixed expiration deletes sessions after a set time regardless of activity. A 30-minute session expires 30 minutes after login even if the user is actively clicking around. Sliding expiration resets the TTL on each request, so sessions only expire after 30 minutes of inactivity. Active users stay logged in indefinitely.

Implement sliding expiration by calling EXPIRE with the full TTL duration on every request that touches the session. This is cheap: EXPIRE is O(1) and adds negligible latency. Combine it with either the string or hash approach depending on your session structure needs.

The tradeoff is that very active users never log out automatically. For security-sensitive applications, you might want both a sliding expiration for inactivity and an absolute maximum session lifetime. Track the creation timestamp in the session and enforce a hard limit regardless of activity. Financial applications often require re-authentication after a few hours even with continuous use.

# On each request, refresh the session TTL
EXPIRE session:abc123 1800
> 1 (TTL reset to 30 minutes)

# Combine with GET in a pipeline for efficiency
GET session:abc123
EXPIRE session:abc123 1800

# For hash sessions, same approach
HGETALL session:abc123
EXPIRE session:abc123 1800

# Check when session was created (for absolute timeout)
HGET session:abc123 created_at
> "1699900000"
# If now - created_at > max_lifetime, force logout

Choosing an approach

Use basic session storage with strings when your session data is simple, you always read and write the entire session, and your web framework already handles serialization. This is the most common approach and works well for most applications.

Use structured sessions with hashes when different parts of your application update different session fields independently, when you want to inspect sessions directly in Redis, or when you have many fields and want to avoid serializing the entire session on every update.

Use sliding expiration when you want sessions to persist as long as users are active. Combine it with an absolute maximum lifetime for security-sensitive applications. The EXPIRE call adds negligible overhead and can be pipelined with your session read.

All three approaches scale well. Redis handles millions of sessions without issue, and lookups are sub-millisecond. The main failure mode is Redis unavailability: if Redis goes down, all sessions are lost. For critical applications, configure Redis persistence or use Redis Cluster for replication.