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_store/lib/echo_store/table.ex (put/3, SET…PX, gate/2) · bcs.4.md (the L1/L2 hit costs) — every figure verbatim from the as-built code or a committed record.
The encodings — Hash, String, JSON
source · Hash / String / JSONA 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
| Encoding | Read | A single-field update | Best for |
|---|---|---|---|
Hash (HSET/HGETALL) | HGETALL, or one field with HGET | HSET one field — no re-serialize | flat attributes touched independently |
String (SET/GET) | GET the whole value | rewrite the whole value | a small opaque blob |
| JSON in a String | GET + parse | parse, edit, re-serialize, SET | nested 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.
Sliding expiration
source · Sliding ExpirationSessions 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.
Session creation flow
source · Session Creation FlowCreating a session is four steps:
- Generate a unique session id — a UUID or a cryptographically secure random string. In EchoStore a
session keys to a branded
SESid minted at sign-in. - Store the session data with its initial fields.
- Set the expiration time.
- 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 signed-in session.
Logout & real-time invalidation
source · Logout & InvalidationLogging 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.
Session data cleanup
source · Session Data CleanupExpired 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.
Security considerations
source · SecurityA 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, andSameSite.
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.
On EchoStore — a TTL’d branded row
table.ex:290, :90, :495 · verbatimA session is the textbook TTL’d row, and EchoStore stores one without any auth surface of its own.
EchoStore.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
EchoStore.Table.invalidate/3 — a DEL on L2 plus an :ets.delete.
The tie to a player 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. codemojex
holds the same shape one level down: a player’s per-round state is a per-player hash
cm:{round}:lock:{player} (Codemojex.Locks — field = position, value = code), so
a locked position persists across guesses with no client state to lose, keyed by the
player’s USR brand.
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 player lock →
Codemojex.Locks.
SET … PX against live Valkey
EchoStore 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:
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.
The three dives
encode → expire → bindEach 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.
Hash, String & JSON
The encoding choice: a HSET Hash for field-level reads and writes, an opaque
SET string for one blob, or JSON for nested data — and the framed version<>value
EchoStore holds.
TTL expiry
A fixed deadline from sign-in against a sliding window refreshed on each access, plus the lazy and
active eviction and EchoStore’s jittered expires_at — how a key that runs out is
reclaimed.
The auth tie-in
A session keyed by a branded SES id via put/3, dropped on logout by
invalidate/3, refused at the kind gate — so a session key can only ever be a session.
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.
A TTL’d branded-id row in an EchoStore 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.
References
sources · relatedSources
- 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 wayEchoStore.Table.putwrites 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
- R1.06.1 · Hash, String & JSONthe encoding choice
- R1.06.2 · TTL expiryabsolute, sliding, and eviction
- R1.06.3 · The auth tie-inthe SES key, logout, the kind gate
- R1 · Cachingthe chapter
- R0 · OverviewValkey under codemojex
- /echomq/cachethe EchoStore near-cache sessions live in, in depth