Where Sessions Live
Process memory, a database table, a shared cache or a distributed store — four answers with different scale ceilings and different things that happen when they fail.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind has a problem.
Where should session state actually be kept, and what breaks at each choice?
Users must stay logged in across deploys, across instances and across regions, and the login lookup must not become the slowest thing in every request.
Keep a map in the process. It is a hash-map read, it is free, and it works perfectly on a laptop and on the single instance that is in production today.
A second instance is deployed. Half the requests find no session, and users are logged out at random depending on which instance the load balancer picked (Load Balancing, From the Backend's Side).
- A second instance is deployed. Half the requests find no session, and users are logged out at random depending on which instance the load balancer picked (Load Balancing, From the Backend's Side).
- Every deploy logs out every user, because the map dies with the process — and deploys become an event the support team notices (Graceful Shutdown).
- The map has no eviction, so it grows for as long as the process lives and is diagnosed months later as a memory leak (Memory Leaks in Backend Services).
- Sticky sessions are turned on to make it work. Then an instance is replaced during a rolling deploy and its users are logged out anyway, and load is now unevenly distributed (Sticky Sessions).
- Moving to a shared store, the store becomes a hard dependency on every request, and its first outage is a full outage with no degraded mode.
What is actually happening
- The session store is read on essentially every authenticated request. Its latency is added to every request, its availability is multiplied into yours, and its capacity is a function of concurrent users rather than of request rate.
- Sessions are small, short-lived, key-addressed, and overwhelmingly read. That access pattern is exactly what a key-value store is for, and it is a poor fit for a relational table only in cost, not in correctness.
- Expiry has to come from somewhere. A store with native TTL expires records for you; a table needs a sweeper job, and a forgotten sweeper is how a session table reaches hundreds of millions of dead rows (Scheduled Jobs).
- Durability and availability are separate questions here. Losing sessions logs people out, which is an annoyance; being unable to read sessions stops the service, which is an outage. The second is the one to design around.
- Distribution costs consistency. Once sessions are replicated across regions, "log out everywhere" is only as immediate as replication is, and a session revoked in one region can survive briefly in another (Eventual Consistency in Practice).
Four stores, four ceilings, four failures
Read this table by the last two columns. The scale ceiling tells you when you will have to move; the failure behaviour tells you what your users experience the day the store has a bad hour. Those are the two facts that actually differ.
| Where | Read cost | Scale ceiling | What happens when it fails |
|---|---|---|---|
| Process memory | A map lookup | One instance that never restarts — so, effectively none | Every deploy and every crash logs out that instance's users |
| Sticky sessions + memory | A map lookup | Works until an instance is replaced or scaled in | Uneven load; users on a replaced instance are logged out; hard to reason about |
| Database table | One indexed primary-key read | Bounded by the database you already depend on | Sessions fail exactly when everything else already is — one dependency, not two |
| Shared in-memory store | One network round trip | Very high; sized by concurrent users, not requests | Total authentication outage unless replicated; eviction policy can silently log people out |
| Distributed / multi-region store | A local-region round trip | Effectively unbounded | Survives a region; revocation becomes eventually consistent, which weakens the model's main advantage |
Choosing, and knowing what would change your mind
The order below is deliberately conservative: use what you already run until a measurement says otherwise. Adding a store is adding a system to operate, secure, monitor and pay for, and "everyone uses one" is not a measurement.
What should move you is a specific signal, named in advance. That is the difference between an architecture decision and an architecture habit.
How many instances, how much traffic, and how immediate must revocation be?
when Local development and tests only.
cost Wrong the moment there are two processes; do not let it reach production by inertia.
when One service, one region, session reads are a small share of database load.
cost An extra read per authenticated request on your busiest dependency; you must build expiry yourself.
when The database read is measurably hurting, or you need very high read throughput.
cost A new dependency to run and secure; eviction policy must be set for session semantics, not cache semantics.
when Users are served from several regions and cross-region session latency is real.
cost Revocation becomes eventually consistent; you must state the window and design around it.
when You want no per-request lookup at all and can accept bounded-delay revocation.
cost Immediate revocation is gone; you inherit key rotation, clock skew and a denylist if you want it back (Token Authentication and the Revocation Problem).
The failure policy is the part nobody writes down
Every team can say where their sessions live. Very few can say what happens when that store returns an error, and the code usually answers the question by accident in a catch block written during a different incident.
There are exactly three possible behaviours and only one of them is defensible. Write it as code, test it by making the store unreachable in a staging environment, and treat a change to it as a security change.
1async function authenticate(req: Request): Promise<Principal> {2 const raw = readSessionCookie(req)3 if (!raw) return anonymous4 5 try {6 const rec = await sessions.get(hash(raw)) // network call, can fail7 return rec ? principalFrom(rec) : anonymous8 } catch (err) {9 // (a) return anonymous -> silently signs everyone out; every10 // authenticated route now 401s and looks like a bug11 // (b) return cachedPrincipal(raw) -> authenticates without checking.12 // An availability incident just became an auth bypass.13 // (c) fail closed, loudly:14 throw new DependencyUnavailable('session store', { cause: err })15 // -> 503 with Retry-After, alert fires, nothing is authenticated16 // on a guess, and the incident is legible ([[error-taxonomy]])17 }18}Option (b) is never written on purpose; it appears when someone adds a "resilience" cache in front of the store without noticing that the cache is now the authenticator.
How to build it
Most important first.
- Start with whatever store you already operate. If there is a database and no cache, a sessions table is a legitimate first answer and avoids adding a dependency you would have to run (When Not to Cache makes the general version of this argument).
- Move to a shared in-memory store when the per-request read is measurably hurting the database or the pool, and not before (Connection Pools).
- Store the id hashed, not raw, so a leak of the store is not a leak of live credentials.
- Set a TTL on every record, and make the absolute lifetime the ceiling regardless of activity (TTL and Expiry).
- Keep records small. A session holds identity, not application data; large sessions turn every request into a large read and a deserialization cost (What Serialization Costs).
- Decide the failure policy explicitly and write it down: if the store is unreachable, the service fails closed and returns 503 for authenticated routes. Serving requests as anonymous or as authenticated-without-checking are both worse.
- Design "log out everywhere" as a first-class operation: either delete by subject index, or carry a per-subject generation number that invalidates all records at once.
- Prefer replication and failover for the store over a local cache of session lookups; caching a session lookup reintroduces the revocation delay that server-held sessions exist to avoid (Local vs Distributed Cache).
What can go wrong
- A shared cache configured with an eviction policy that discards sessions under memory pressure, logging users out on a traffic spike — the store was sized for cache semantics, not for session semantics.
- No TTL on a database-backed store and no sweeper, so the table grows without bound and eventually the index does too.
- A store outage with a fail-open handler, which turns an availability incident into an authentication bypass (Fail Open vs Fail Closed in Security Engineering).
- Session data used as application state — a cart, a wizard, a large permission blob — so the store becomes a database that nobody backs up.
- Cross-region replication lag making revocation non-immediate while the design document still claims sessions revoke instantly.
- Local caching of session lookups for performance, which silently converts immediate revocation into TTL-bounded revocation — the exact trade-off of Token Authentication and the Revocation Problem, adopted by accident.
- Concurrent requests sliding the idle clock write the same record; without an atomic field update, one write can undo the other and shorten the session (Atomic Operations).
- A revocation racing an in-flight lookup: a request that read the record before deletion proceeds with a valid principal. The window is one request duration, plus any cache TTL in front of the store.
- Two logins for the same user creating sessions concurrently is fine by design — sessions are per device — which is why "one session per user" is a product decision that needs a deliberate atomic implementation, not an assumption.
- A dumped session store is a set of live credentials unless the ids are stored hashed. Hash them with a fast hash — the id is already high-entropy random, so slowness buys nothing here, unlike a password (Credentials and Password Handling).
- Encrypt the store's traffic. A shared cache reached over an unauthenticated connection on a flat network is a credential feed (Network Segmentation in Security Engineering).
- Fail closed on store errors, always. This is the one place where an availability incident can become an authentication bypass.
- Revocation must reach every place a session can be resolved. Any read-through cache in front of the store extends the life of a revoked session by its TTL.
- In a multi-tenant system, ensure a session record carries the tenant and that lookups cannot cross tenants (Tenant Isolation).
- "Sticky sessions fix in-memory sessions." They make the failure less frequent and more confusing. Instances still restart, scale in, and get replaced (Sticky Sessions).
- "Redis is the answer." It is a good answer for a specific problem — a fast shared key-value store with TTL — and it is a dependency you must operate. A sessions table is a legitimate answer for many services.
- "Sessions in the database are too slow." Measure it. It is an indexed primary-key read, and for a great many services it is a small fraction of the request.
- "The store is a cache, so losing it is fine." Losing it logs out every user simultaneously, which for most products is an incident.
- "Encrypted cookie sessions mean no store at all." They mean the state moved to the client and revocation became a problem — that is Token Authentication and the Revocation Problem wearing a cookie.
Operating it
- Store latency at p50 and p99 as a dependency metric on the request path, plus error rate and timeout count.
- Live session count and creation rate. A creation spike without a login spike means sessions are being created and lost.
- Miss rate on lookups, split into "expired" and "not found" — the second is the one that indicates a real problem.
- Store memory or table size against the configured TTL, so you learn that expiry has stopped working before the disk does.
- For replicated stores, measure replication lag directly and state the revocation window it implies.
- At one instance, memory works and is a trap. At two, it does not work at all. That transition is abrupt and is the single most common cause of "random logouts" reports.
- At 10x, the store is a hot dependency: the read is per request, so its throughput requirement scales with traffic while its size scales with concurrent users.
- At 100x or multi-region, the questions become replication topology and the revocation window, and this is the point where teams reconsider signed tokens — trading immediate revocation for the removal of the lookup (Token Authentication and the Revocation Problem).
- Process memory is fastest and only correct for a single instance that never restarts, which is not a production system.
- A database table adds no new dependency and puts an extra read on the resource that is usually already the bottleneck.
- A shared in-memory store is fast and well-matched and is one more system to run, secure, monitor and pay for. It is not a thing every backend needs (When Not to Cache).
- A globally distributed store gives low latency everywhere and makes revocation eventually consistent, which weakens the main advantage sessions had over tokens.
Where this applies
Backend advice is context-sensitive. These labels say what each claim is specific to, and where a different stack or scale would differ.
- GENERALThe four options and their trade-offs are stack-independent; only the names of the products change.
- SCALE-SPECIFICBelow two instances, in-process memory genuinely works, and the reason to avoid it is that the transition to two instances is abrupt rather than gradual. Above a single region, the distributed option's revocation window becomes the dominant concern, which is a different argument entirely.
- CLOUD-SPECIFICManaged offerings differ in the properties that matter here: a managed cache may evict under memory pressure by default (losing sessions), while a managed multi-region key-value store gives durability with asynchronous replication (delaying revocation). Both are called "the session store" and they fail in opposite directions — check the eviction policy and the replication mode, not the product category.
Where the depth lives
This domain teaches the application-side mechanism and hands the rest off.