jonnify·redis /redis-patterns/caching/session-management

R1.06 · Caching — module hub

Session management.

Store user sessions in Valkey with automatic expiration using TTL, choosing between Hashes (field-level access), Strings (serialized), or JSON (nested data) based on access patterns.

Grounding: echo/apps/echo_cache/lib/echo_cache/table.ex (put/3, SET…PX, gate/2) · content/bcs4.1.md (the L1/L2 hit costs) — every figure verbatim from the as-built code or a committed record.

A session’s three moves. Select a moment to read the command that runs and what becomes of the key and its deadline.
sign inHSET + EXPIRE a requestHGETALL + EXPIRE log outDEL TTL window — refreshed on each read, run out when idle, dropped on logout
readout · Select a moment. A session is state with a deadline: written on sign-in, read on every request, ended by a logout or an idle clock.
§1

The encodings — Hash, String, JSON

source · Hash / String / JSON

A session record holds a few attributes about one signed-in user — a user id, a sign-in time, a last-seen time — and it can sit under a key three ways. The choice is an access-pattern trade-off.

A Hash stores the attributes as named fields, so one field reads or updates without touching the rest. A String holds the whole record as one opaque value, written and read whole. For nested data — a cart, a preferences map — serialize to JSON and store that string with an expiry. The JSON form simplifies nested data but requires reading and writing the entire session on each update.

# a Hash: named fields, addressed one at a time
HSET session:abc123 user_id "1" created_at "1706648400" last_access "1706648400"
HGETALL session:abc123                 # read the full session
HSET session:abc123 last_access "1706648500"   # update one field

# a String holding JSON: one opaque value, read and written whole
SET session:abc123 '{"user_id":1,"cart":["item1","item2"],"prefs":{"theme":"dark"}}' PX 1800000
EncodingReadA single-field updateBest for
Hash (HSET/HGETALL)HGETALL, or one field with HGETHSET one field — no re-serializeflat attributes touched independently
String (SET/GET)GET the whole valuerewrite the whole valuea small opaque blob
JSON in a StringGET + parseparse, edit, re-serialize, SETnested structure (cart, prefs)

The trade-off is field-level access (the Hash) against one round trip for the whole blob (the String or JSON). The first dive measures what a single-field update costs in each.

§2

Sliding expiration

source · Sliding Expiration

Sessions should typically expire after a period of inactivity, not from creation time. The fix is to reset the TTL on each access: the request reads the record, and an EXPIRE slides the deadline a full window forward.

HGETALL session:abc123                 # the request reads the record
EXPIRE session:abc123 1800             # and slides the deadline 1800s ahead of now

This creates a sliding window: an active user stays signed in because each request pushes the deadline ahead, while an inactive session runs out the clock and expires on its own — no cron job, no sweep, no application bookkeeping. The second dive contrasts this with a fixed deadline and shows how the engine reclaims the key.

§3

Session creation flow

source · Session Creation Flow

Creating a session is four steps:

  1. Generate a unique session id — a UUID or a cryptographically secure random string. In EchoCache a session keys to a branded SES id minted at sign-in.
  2. Store the session data with its initial fields.
  3. Set the expiration time.
  4. Return the session id to the client, typically as a cookie.

The session id must be cryptographically random to prevent guessing attacks — a guessable id is a way into someone else’s account.

§4

Logout & real-time invalidation

source · Logout & Invalidation

Logging out a single session is two deletes — the session key and its entry in the user’s roster Set:

DEL session:abc123
SREM user:1:sessions abc123

When a user logs out or an admin revokes access, every application server must immediately stop accepting the session. Pub/Sub broadcasts the invalidation so a stale copy in any server’s local cache is dropped at once.

PUBLISH session:invalidate abc123      # on logout, the publisher fans out the id
SUBSCRIBE session:invalidate           # every app server drops its local copy on a message

Logout is a delete, and at scale a publish: DEL the key, then PUBLISH the id so no server keeps serving a session that no longer exists.

§5

Session data cleanup

source · Session Data Cleanup

Expired session keys are reclaimed by Valkey automatically, but the session-id references in a user’s Set can go stale — they name keys that no longer exist. A background job, run during low-traffic periods, reconciles them: iterate the user:*:sessions keys, and for each id check whether session:{id} still exists, removing the references that point at nothing.

§6

Security considerations

source · Security

A session store has a small, fixed security checklist:

  • Generate session ids with a cryptographically secure random generator.
  • Never store a password or any sensitive credential in the session.
  • Serve session-id cookies over HTTPS only.
  • Consider storing a hash of the user’s IP or user-agent to detect session hijacking.
  • Set the cookie flags HttpOnly, Secure, and SameSite.

Keep the record lean and the id unguessable: a session holds the user id and an opaque token, never a credential, behind a cryptographically random id in a hardened cookie.

§7

On EchoCache — a TTL’d branded row

table.ex:290, :90, :495 · verbatim

A session is the textbook TTL’d row, and EchoCache stores one without any auth surface of its own. EchoCache.Table.put/3 sets both layers under the declared TTL, keyed by a branded SES id and framed with the write’s mint-time version — SET ecc:{sessions}:<id> (version<>value) PX ttl_ms (table.ex:290). A re-auth re-puts with a fresh version (put/3 mints BrandedId.generate!(spec.kind)); logout is EchoCache.Table.invalidate/3 — a DEL on L2 plus an :ets.delete.

The tie to authentication is the kind gate, not a credential check the cache owns. Before either layer is touched, gate/2 (table.ex:495) requires the id be 14 bytes, of the table’s declared namespace, and valid — so a USR id can never be used as a SES key, and a wrong-namespace id is refused at the door with zero keys on the wire. The Exchange Platform carries an opaque account id (Exchange.Gateway.parse_account/1); a session keys to the account it admits.

Place the fact. Select a fact from the as-built cache; the readout names the function or law it lives in.
readout · Select a fact. Each is quoted from the real EchoCache or Exchange surface, and each lives in exactly one function or law.

Static verdicts — SET…PX → put/3, table.ex:290 · version<>value → the framed write, table.ex:296 · the kind gate → gate/2, table.ex:495 · ecc:{sessions}:id → Keyspace.key/2 · the account id → Exchange.Gateway.parse_account/1.

the application · the session row, value and TTL in one command

SET … PX against live Valkey

EchoCache writes the L2 row with its expiry in one command, so the second layer expires itself even if every node forgets — the committed cache record measured against live Valkey:

content/bcs4.1.md · EchoCache cache-aside, against live Valkeyfrozen
L1 hit reads                  1311621 / s   ← 762 ns each, caller-side ETS
L2 GET on the same wire       31 us          ← the round trip a hit replaces
the L1 hit is                 40 times cheaper
the L2 row                    SET … PX, PTTL 300 ms of 300

Notes on ValkeySET … PX sets the value and the TTL atomically; a separate EXPIRE after the SET leaves an immortal key if the process dies between the two — valkey.io/commands/set.

§8

The three dives

encode → expire → bind

Each dive takes one of the three choices a session store has to settle, in the arc encode → expire → bind: which shape holds the record, how the deadline is set and reclaimed, and how the session’s life is tied to the namespace it belongs to. Read them in order — the shape comes first.

the pattern

A session is per-user state under a random key, encoded as a Hash or a string, given a TTL that ends it on logout or after inactivity. The engine reads it on every request and reclaims it when the deadline passes.

its EchoCache application

A TTL’d branded-id row in an EchoCache table: put/3 writes SET…PX framed with a mint-time version, invalidate/3 drops it on logout, and the kind gate binds the key to one namespace.

Three dives, one store: choose the shape, set the deadline, and bind the life to the namespace it represents. Session management is the last cache pattern in R1.

§9

References

sources · related

Sources

  • Redis — Documentation — hashes, strings, key expiry, and the data structures a session store is built from.
  • Redis — HSET — set one or more fields of a Hash; the field-level write a Hash session uses.
  • Valkey — SET — set a key to one value with PX; value and TTL set atomically, the way EchoCache.Table.put writes L2.
  • Valkey — EXPIRE — set a key’s time-to-live; the deadline that ends an idle session on its own.
  • Sanfilippo, S. — antirez weblog — the Redis creator on key expiry and how the engine reclaims keys that have run out.

Related in this course