WEB / WEB-13.02
Session stores and distributed sessions
The Browser can carry the same session handle while requests reach different application nodes. A shared store centralizes the binding; consistency, expiry, and failure decisions still need design.
1. Core · Mechanism
Read every scenario without motion
Current binding
- Browser request has already reached B
The scene begins immediately before store lookup. Handle-verification input is received; no credential is shown.
- Browser / Duru
- Waiting for request result
- Application Node A
- Idle; unused for this request
- Application Node B
- Received request; session unresolved
- Shared session store
- ledger-duru: active v7
- Lookup reached the store
Node B’s driver/protocol lookup arrives at the remote store. The record is still active v7.
- Browser / Duru
- Waiting for request result
- Application Node A
- Idle; unused for this request
- Application Node B
- Lookup pending; session unresolved
- Shared session store
- Lookup received; active v7
- Binding reply reached B
The store returned the active v7 binding and policy fields. B must evaluate them.
- Browser / Duru
- Waiting for request result
- Application Node A
- Idle; unused for this request
- Application Node B
- Resolved: active v7; lifetime check pending
- Shared session store
- ledger-duru: active v7
- Session and resource decisions pass
In this branch, lifetime is valid and separate resource authorization is supplied as allowed. An HTTP response can now be prepared.
- Browser / Duru
- Waiting for request result
- Application Node A
- Idle; unused for this request
- Application Node B
- Session accepted; resource allowed
- Shared session store
- ledger-duru: active v7
- Application response reached the Browser
The Browser learns the request result. The store wire and the HTTP response use different connections.
- Browser / Duru
- Protected request accepted
- Application Node A
- Idle; unused for this request
- Application Node B
- Response: 200
- Shared session store
- ledger-duru: active v7
Revoked · old touch
- A’s old snapshot
A previously saw active v7; before this case began, B’s logout made the authoritative record revoked v8.
- Browser / Duru
- Waiting for A request
- Application Node A
- Old snapshot: active v7
- Application Node B
- Earlier logout committed v8
- Shared session store
- Authoritative: revoked v8
- Conditional touch reached the store
A requests a touch with expected version 7. The remote receiver’s current record remains v8.
- Browser / Duru
- Waiting for A request
- Application Node A
- Touch pending; expected v7
- Application Node B
- Earlier logout committed v8
- Shared session store
- Touch received: expected7/current8; revoked
- Atomic condition rejects mutation
The version does not match and the record is revoked. The store does not write the old active snapshot; v8 is unchanged.
- Browser / Duru
- Waiting for A request
- Application Node A
- Touch pending; expected v7
- Application Node B
- Earlier logout committed v8
- Shared session store
- Rejected; revoked v8 unchanged
- Rejection reached A
The authoritative reply reports current revoked v8; A cannot rely on its old snapshot.
- Browser / Duru
- Waiting for A request
- Application Node A
- Current resolved: revoked v8; reject session
- Application Node B
- Earlier logout committed v8
- Shared session store
- Rejected; revoked v8 unchanged
- Session rejection reached the Browser
The fixture returns HTTP 401; it does not recreate an authenticated binding.
- Browser / Duru
- Session invalid; authenticated processing rejected
- Application Node A
- Response: 401
- Application Node B
- Earlier logout committed v8
- Shared session store
- Rejected; revoked v8 unchanged
Lookup error
- Lookup is required
B has the Browser request. B has no current binding; the store service is unreachable in this branch.
- Browser / Duru
- Waiting for request result
- Application Node A
- Idle
- Application Node B
- Session unresolved; lookup required
- Shared session store
- Unreachable; authoritative state not observed
- Lookup failure observed
The driver reports an error; there is no completed remote arrival/reply. No wire movement is invented.
- Browser / Duru
- Waiting for request result
- Application Node A
- Idle
- Application Node B
- Lookup error; current state unknown
- Shared session store
- Unreachable; authoritative state not observed
- Authenticated processing is not accepted
Sufficient trusted state was not resolved. The error is not relabeled as a missing record or expired session.
- Browser / Duru
- Waiting for request result
- Application Node A
- Idle
- Application Node B
- Fail closed; service failure response prepared
- Shared session store
- Unreachable; authoritative state not observed
- Service failure reached the Browser
The fixture chooses HTTP 503. The Browser cannot infer certain login expiry from this response.
- Browser / Duru
- Service unavailable; retry later
- Application Node A
- Idle
- Application Node B
- Response: 503
- Shared session store
- Unreachable; authoritative state not observed
ACTORS · STATE · DATA FLOW
Same handle, different application node
In WEB-13.01, an opaque Browser handle resolved to a server-side binding. Now let one Duru request reach Node A and the next reach Node B. Seeing the same handle text does not automatically prove that the nodes see the same authenticated record.
This lesson covers an opaque server-store session design. Its record contains server-managed information such as a verified subject, tenant context, lifetime, and revocation state. The Browser handle carries a reference; authoritative record fields are not reconstructed from the client.
The fixture’s ledger-duru is an audit-correlation alias, not a credential. Neither a wire credential nor a verifier is shown. A Duru/Atlas binding established by the earlier secure login flow is a supplied starting condition.
What does process memory provide and lose?
A single application process can keep session objects in its own memory. This is simple, but another process does not automatically share that area. After a restart, a record held only in volatile memory does not return as though it were durable disk data.
A load balancer using sticky sessions can route Duru to Node A, sending requests to the same node-local design. This routing choice does not create shared session authority. If A disappears, B may lack the record; routing and state durability are different problems.
Make “Where does the server keep the session?” concrete: which process or service owns it, what is its lifecycle, and what is its loss boundary? Saying “server-side” alone does not explain restart, scaling, or failover behavior.
Shared session store: a common record, separate request decisions
Nodes A and B can resolve the same binding through a shared store. When the store is a separate service, application↔store messages use a driver/protocol connection distinct from Browser↔Application HTTP. The scene starts after Node B has received a Browser request and depicts the remote lookup.
A successful lookup does not alone establish that the record may be used. The Application checks active/revoked state and server-side lifetime policy, then applies authorization for the target resource. Session recognition and resource permission are separate decisions.
Many store technologies can serve this role. Saying “we use Redis” or “we use a database” does not settle access isolation, consistency, persistence, or failure policy. This lesson uses Redis documentation to illustrate particular trade-offs, without assigning its defaults to every store.
Stale state: an old active copy must not overwrite revocation
In another fixture branch, Node A previously read a version-7 active snapshot. Node B’s logout flow changed the shared record to version 8 revoked. A’s snapshot is a historical observation, not current authority.
If A updates lastSeen from its old snapshot by unconditionally overwriting the entire record, it can lose the revocation. A defensive update is accepted atomically only while the expected version still matches, the record is active, and lifetime policy allows it. Field updates must preserve the same policy boundary.
The fixture’s conditional touch compares expected=7 with current=8, performs no mutation, and preserves the revoked record. On version conflict, reread current state and reapply policy; do not blindly retry to rewrite the old active state. Redis WATCH/CAS is an example of conditional updating; this scene executes no Redis command sequence.
How long a tombstone or generation/version strategy remains depends on lifecycle design. Preventing an old writer from recreating a deleted session record is part of the same boundary; a missing record does not permit an authenticated upsert.
Evaluate consistency separately from durability
Using a shared endpoint does not guarantee that every reader instantly sees the latest state. Basic Redis replication is asynchronous by default. A replica-read strategy can expose older state; the acceptable consistency boundary for revocation needs an explicit decision.
Redis WAIT provides acknowledgements from the specified number of replicas; it does not automatically turn the system into a strongly consistent CP system. Acknowledged writes can still be lost during failover depending on persistence configuration. Do not equate one acknowledgement with durability against every crash and failover.
For persistence, RDB offers point-in-time snapshots, while AOF uses a write log and startup replay; persistence can also be disabled. These choices have different loss and recovery boundaries. If login or logout is reported as successful, the required store acknowledgement must follow the product’s security and availability policy.
Expiry, eviction, missing, and failure are different outcomes
Session expiry is the application’s server-side lifetime decision. A store TTL can help clean up obsolete records; a physically present record is not usable after its policy deadline. Do not rely solely on cleanup timing to enforce expiry.
Eviction is data loss caused by capacity policy. Redis eviction policies can remove keys at a memory boundary. noeviction avoids that removal but can cause new-data writes to fail; it does not provide unlimited capacity or availability. When the session store holds the only authoritative copy, a missing record cannot be treated as an ordinary cache miss rebuilt from the client.
A successful lookup reporting “no record” differs from a lookup service that cannot answer. The former reports no valid binding for that handle; the latter leaves current state unknown. This fixture accepts authenticated processing in neither case and does not label service failure as expiry.
A cookie still present in Duru’s Browser does not restore a lost server binding. A new authenticated session can be established only through a separate valid authentication flow; an old client claim cannot replace the missing record.
2. Red · Trust & Impact
Read every scenario without motion
Stale touch
- The node input is prepared
Node A has a historical active snapshot at version 7. The current store is version 8 revoked; the node has not learned it through a fresh lookup yet.
- Application node
- Expected version 7 touch ready
- Remote session store
- Current version 8 revoked
- Acceptance gate
- Session decision pending
- The operation reached the remote store
The operation from node A reached the shared authoritative store. This is an application-to-store message, not browser HTTP; driver protocol details are unspecified.
- Application node
- Awaiting store outcome
- Remote session store
- Expected 7 touch received
- Acceptance gate
- Session decision pending
- The authoritative condition was evaluated
The conditional update compares expected version 7 with current version 8. Active-state and lifetime conditions are also required; a revoked record is not overwritten by an old active snapshot.
- Application node
- Awaiting store outcome
- Remote session store
- Version 8 revoked retained
- Acceptance gate
- Session decision pending
- The store outcome was prepared
The store prepares conditional-write-rejected for return to the node. At this stage the node has not yet received the new outcome.
- Application node
- Awaiting store outcome
- Remote session store
- Rejected; version 8 unchanged
- Acceptance gate
- Session decision pending
- The store outcome reached the node
Node A received the version conflict and revoked state. Old local state is not used as current authenticated authority.
- Application node
- Current revoked known
- Remote session store
- Version 8 unchanged
- Acceptance gate
- Session decision pending
- Authenticated processing was withheld
sessionAccepted remains false. A conflict requires rereading trusted state and reapplying policy; blind retries do not justify resurrecting an old snapshot.
- Application node
- Session accepted=false
- Remote session store
- Revoked version 8 unchanged
- Acceptance gate
- Resource processing not accepted
Missing record
- The node input is prepared
Node B prepares alias context for an ordinary lookup. A handle remaining in the browser does not establish that the authoritative store contains a record.
- Application node
- Lookup ready; client handle alias
- Remote session store
- Authoritative record missing
- Acceptance gate
- Session decision pending
- The operation reached the remote store
The operation from node B reached the shared authoritative store. This is an application-to-store message, not browser HTTP; driver protocol details are unspecified.
- Application node
- Awaiting store outcome
- Remote session store
- Lookup received
- Acceptance gate
- Session decision pending
- The authoritative condition was evaluated
The successful lookup returns no record. OnMissing policy does not create an authenticated binding from a client claim; missing here is not a transport failure.
- Application node
- Awaiting store outcome
- Remote session store
- Record missing; no upsert
- Acceptance gate
- Session decision pending
- The store outcome was prepared
The store prepares success-missing for return to the node. At this stage the node has not yet received the new outcome.
- Application node
- Awaiting store outcome
- Remote session store
- Missing outcome ready
- Acceptance gate
- Session decision pending
- The store outcome reached the node
Node B received the successful lookup’s missing-record outcome. Old local state is not used as current authenticated authority.
- Application node
- Missing binding known
- Remote session store
- Record missing
- Acceptance gate
- Session decision pending
- Authenticated processing was withheld
sessionAccepted remains false. A new session requires independent valid authentication; a missing record is not reconstructed from cookie text.
- Application node
- Session accepted=false
- Remote session store
- No authenticated upsert
- Acceptance gate
- Resource processing not accepted
ASSUMPTION · BOUNDARY · CONTROL
Review more than the happy-path node
In a defensive review, record recognition on Node A separately from recognition on Node B. Lookup source, observed version, and decision belong together. A branch where a local cache shows an old active state helps explain whether current revocation is enforced.
Ask where the boundary lies between a logout response and every reader observing revocation. Network and replica behavior are part of the design. A stale record is not automatically a compromise; its security effect depends on the decision for which the application accepts it.
Store access is also a credential boundary
A session store should not be broadly accessible merely because it is called a performance cache. Reading a store that holds raw random tokens can disclose still-valid credentials. High entropy does not prevent disclosure.
OWASP discusses storing a one-way verifier when read-only store disclosure is in the threat model. This protects against read-only disclosure, not record modification or application compromise. This lesson contains no raw token or verifier; its ledger alias cannot be used as authentication proof.
3. Blue · Secure development & defense
Read every scenario without motion
Policy expiry
- A session-decision row was selected
Review examines the policy-expired row. Both branches have sessionAccepted=false, but their evidence about trusted state differs.
- Decision record
- case=policy-expired
- Lookup review
- Lookup not examined
- Evidence conclusion
- Reason not established
- The lookup source was separated
Lookup is successful and source=authoritative; observedVersion=7 is known. An active physical record still needs a separate application lifetime-policy evaluation.
- Decision record
- case=policy-expired
- Lookup review
- Success; authoritative; version 7
- Evidence conclusion
- Reason not established
- Lifetime evidence was evaluated
lifetimeValid=false explains the policy-expired outcome. Physical record retention does not preserve valid session authority; TTL cleanup is a separate mechanism.
- Decision record
- case=policy-expired
- Lookup review
- Lifetime invalid; record present
- Evidence conclusion
- Expiry known
- The fail-closed decision was bounded
The artifact does not accept authenticated processing. The decision rests on known expiry; the two reasons are not collapsed into one logout/expiry label.
- Decision record
- case=policy-expired
- Lookup review
- sessionAccepted=false
- Evidence conclusion
- Known expiry
- The selector result was read in scope
The main selector searches for accepted=true with missing known/active/lifetime conditions and returns 0. A separate positive fixture returns 1; these rows are not actual Redis metrics or latency measurements.
- Decision record
- case=policy-expired
- Lookup review
- Integrity count=0; positive=1
- Evidence conclusion
- Reason remains distinct
- No recovery claim was added
This row explains session-policy expiry; eviction, crash or replication causes are not inferred from it.
- Decision record
- case=policy-expired
- Lookup review
- No authenticated acceptance
- Evidence conclusion
- Policy expiry known
Store error
- A session-decision row was selected
Review examines the store-unavailable row. Both branches have sessionAccepted=false, but their evidence about trusted state differs.
- Decision record
- case=store-unavailable
- Lookup review
- Lookup not examined
- Evidence conclusion
- Reason not established
- The lookup source was separated
Lookup is an error with source=unknown and observedVersion=null. The application has not learned the current record state.
- Decision record
- case=store-unavailable
- Lookup review
- Error; unknown source/version
- Evidence conclusion
- Reason not established
- Lifetime evidence was evaluated
A store failure establishes neither lifetime nor revocation state. The service-unavailable product outcome does not mean expiry was observed.
- Decision record
- case=store-unavailable
- Lookup review
- Current lifecycle unknown
- Evidence conclusion
- UNKNOWN: lifecycle
- The fail-closed decision was bounded
The artifact does not accept authenticated processing. The decision rests on failure to resolve trusted state; the two reasons are not collapsed into one logout/expiry label.
- Decision record
- case=store-unavailable
- Lookup review
- sessionAccepted=false
- Evidence conclusion
- UNKNOWN: record state
- The selector result was read in scope
The main selector searches for accepted=true with missing known/active/lifetime conditions and returns 0. A separate positive fixture returns 1; these rows are not actual Redis metrics or latency measurements.
- Decision record
- case=store-unavailable
- Lookup review
- Integrity count=0; positive=1
- Evidence conclusion
- Reason remains distinct
- No recovery claim was added
This row explains a service error; it does not prove that a session record was deleted or expired. Recovery must resolve trusted store state again.
- Decision record
- case=store-unavailable
- Lookup review
- No authenticated acceptance
- Evidence conclusion
- UNKNOWN: current store state
IMPLEMENTATION · SECURITY CONTROL · VERIFICATION
Define authoritative state and read policy
Define the same binding source, expected revocation visibility, and approved read path for every node. If replicas or local caches are used, explicitly limit decisions that may accept stale state; a short cache TTL alone is not a strong-consistency guarantee.
Limit store access to necessary application components and protect transport, data at rest, and backups. Use opaque audit correlation to keep credentials out of observability systems. Evaluate persistence and failover policies together with session-lifecycle expectations.
Make lifetime updates an atomic policy
For touch/refresh, evaluate expected version, active state, and lifetime conditions at one atomic boundary. Reread on conflict; reject turning revoked/missing/expired state into active without valid authentication. A generic upsert does not replace this policy.
Verify conditional primitives’ error semantics in backend documentation. IndexedDB transaction abort rolling back its changes does not mean Redis transactions roll back execution-time errors; Redis does not provide that rollback. Do not assume the word “transaction” grants the same guarantee across technologies.
Make failure safe and understandable
Distinguish missing records, policy expiry, capacity eviction, and service-unavailable observations. This fixture fails closed when it cannot resolve enough trusted state for authenticated processing; it does not present a service error as proof of logout. Retry and recovery cannot invent authenticated authority.
Track capacity eviction, expiry, and write rejection separately. This lesson’s ledger contains no measured latency, throughput, or real Redis metric results; it only explains decisions in its own deterministic branches.
NOW APPLY IT TO THE CASE
Which decision cannot be replaced by a stale snapshot or store failure?
Nodes use a shared authoritative binding. Conditional touches preserve current revocation; unknown lookup state is not presented as expiry.
Find separate observations for the case handle, server binding and session lifetime. Does a retained browser value establish current server admission by itself?
Open the related case file ↗READ THE EXAMPLE TOGETHER
Two nodes and an authoritative session ledger
Select a field and examine what it supports and where the evidence ends.
Read the raw artifact and field explanations
{
"architecture": "opaque handle + remote authoritative session store; driver protocol unspecified",
"auditCorrelation": "ledger-duru; not a credential",
"subject": {
"displayName": "Duru",
"tenant": "Atlas",
"binding": "supplied previously verified authentication; wire token omitted"
},
"cases": [
{
"case": "normal",
"node": "B",
"lookup": "success",
"source": "authoritative",
"observedVersion": 7,
"recordState": "active",
"lifetimeValid": true,
"sessionAccepted": true,
"resourceAuthorization": "separate required decision"
},
{
"case": "stale-touch",
"node": "A",
"lookup": "conditional-write-rejected",
"source": "authoritative",
"expectedVersion": 7,
"observedVersion": 8,
"recordState": "revoked",
"recordAfter": "revoked version 8 unchanged",
"sessionAccepted": false
},
{
"case": "missing-after-eviction",
"node": "B",
"lookup": "success-missing",
"source": "authoritative",
"observedVersion": null,
"recordState": "missing",
"capacityObservation": "authored eviction branch; not a measured Redis event",
"sessionAccepted": false
},
{
"case": "policy-expired",
"node": "B",
"lookup": "success",
"source": "authoritative",
"observedVersion": 7,
"recordState": "active physical record",
"lifetimeValid": false,
"sessionAccepted": false
},
{
"case": "store-unavailable",
"node": "B",
"lookup": "error",
"source": "unknown",
"observedVersion": null,
"recordState": "unknown to application",
"sessionAccepted": false,
"productOutcome": "service unavailable; not proof of expiry"
}
],
"conditionalUpdatePolicy": {
"required": [
"record exists",
"expected version matches",
"record active",
"lifetime valid"
],
"onConflict": "reread trusted state and reapply policy; no blind resurrection",
"onMissing": "do not create authenticated binding from client claim"
},
"selector": {
"xml": "<sessions><decision case=\"normal\" accepted=\"true\" known=\"true\" active=\"true\" lifetime=\"true\"/><decision case=\"stale-touch\" accepted=\"false\" known=\"true\" active=\"false\" lifetime=\"true\"/><decision case=\"missing-after-eviction\" accepted=\"false\" known=\"false\" active=\"false\" lifetime=\"false\"/><decision case=\"policy-expired\" accepted=\"false\" known=\"true\" active=\"true\" lifetime=\"false\"/><decision case=\"store-unavailable\" accepted=\"false\" known=\"false\" active=\"false\" lifetime=\"false\"/></sessions>",
"xpath": "count(/sessions/decision[@accepted=\"true\" and (@known!=\"true\" or @active!=\"true\" or @lifetime!=\"true\")])",
"expected": 0,
"positiveFixtureXml": "<sessions><decision accepted=\"true\" known=\"false\" active=\"false\" lifetime=\"false\"/></sessions>",
"positiveExpected": 1
}
}- lookup / source
- Lookup outcome and freshness source; error, missing, and an older snapshot differ.
- expectedVersion / observedVersion
- The concurrency condition for a conditional update; not a security credential.
- recordState / lifetimeValid
- Physical record presence does not replace active state and valid lifetime.
- sessionAccepted
- This fixture’s session decision, not the resource-authorization result.
- selector
- Queries the known+active+lifetime invariant for acceptance; not actual telemetry.
Assumes one logical authoritative remote store; replica/failover effects are explained but not simulated here. Versions 7/8 are aliases; no latency or TTL figures are supplied. HTTP 401/503 are chosen responses in this fixture. No real store, credential, or load balancer was executed.
Two reality checks
A shared session store guarantees the same latest state instantly on every node and lossless failover.
Read paths, replication consistency, and persistence are separate design decisions. Redis uses asynchronous replication by default; WAIT alone does not guarantee strong consistency or durability through every failover.
A missing store record can be rebuilt as an authenticated session from the cookie; a store error means expiry.
A missing binding cannot be reauthenticated from a client claim. Lookup error leaves current state unknown; expiry is the application’s lifetime decision. This fixture rejects authenticated processing without sufficient trusted state.
YOUR TURN
Work through this story.
Predict the outcome, find the evidence boundary, and explain it in your own words.
NodeA touches expectedVersion=7, but authoritative version 8 is revoked. What is the selected write outcome?
- The version 7 snapshot reactivates revoked state because it was last used.
- Rejected; revoked version 8 remains unchanged.
Read the explained answer
Rejected; revoked version 8 remains unchanged. — A stale touch cannot resurrect a revoked binding; onConflict requires rereading trusted current state.
Does a store-lookup error prove the session expired?
Selected lesson artifact fields; teaching example
- cases[4]
- {"case":"store-unavailable","node":"B","lookup":"error","source":"unknown","observedVersion":null,"recordState":"unknown to application","sessionAccepted":false,"productOutcome":"service unavailable; not proof of expiry"}
- No; the record is unknown, acceptance is withheld, and the service is unavailable.
- Yes; every store error means session expiry.
Read the explained answer
No; the record is unknown, acceptance is withheld, and the service is unavailable. — Source is unknown and productOutcome explicitly says not proof of expiry; missing/expired branches are separate facts.
Why do success-missing after eviction and a physically present lifetime-invalid record differ even though both have accepted=false?
Read the explained answer
Success-missing finds no binding in authoritative lookup and does not recreate it from a client claim. A policy-expired record may remain physically present while its lifetime gate closes. Eviction/durability/replication mechanisms do not replace login policy; shared-store acceptance is also not resource authorization.
□ I separated physical absence from logical expiry.
□ I did not treat store mechanics as authentication policy.
Open the application console · request / response / browser
Also open request, URL, cookie and DOM examples
Sources and scope
- OWASP — Session Management Cheat SheetSession ID Content (or Value) · Built-in Session Management Implementations · Server-Side Session Token Storage · Session Expiration · Logging Sessions Life Cycle
- OWASP — Authorization Cheat SheetValidate the Permissions on Every Request · Verify that Authorization Checks are Performed in the Right Location
- Redis — Key evictionUsing the maxmemory configuration directive · Eviction policies: noeviction · Using the INFO command: evicted_keys versus expired_keys
- Redis — ReplicationRedis replication: asynchronous default and WAIT limitations
- Redis — PersistencePersistence options: RDB, AOF, no persistence · RDB disadvantages
- Redis — TransactionsOptimistic locking using check-and-set · WATCH explained · Errors inside a transaction · What about rollbacks?