Configuring Redis Session Expiry and Eviction
Users report being logged out “randomly”, the pattern makes no sense, and nothing in the application logs looks wrong — because nothing in the application is wrong. Redis is deleting live sessions to stay inside its memory limit. This page is part of the managing session state in Redis guide and covers the configuration that separates an intended timeout from an unplanned eviction.
Root Cause: Two Very Different Deletions
Redis removes a key for one of two reasons, and only one of them is something you asked for.
Check INFO stats for evicted_keys. On a session store the correct value is zero, permanently. Anything else means live credentials are being discarded, and the number will correlate exactly with the complaints.
The Fix: Three Settings and a Number
Set an explicit maxmemory. Without it Redis grows until the operating system intervenes, which on a container platform means the process is killed and every session disappears at once. An explicit limit converts an abrupt outage into a policy decision you control.
Set maxmemory-policy to volatile-lru. Only keys carrying a TTL become eviction candidates, so anything you deliberately persisted without one is safe. allkeys-lru will discard operational keys as readily as sessions; noeviction turns a full instance into write failures on login, which at least fails loudly.
Size memory from a measurement, not a guess. Take the memory footprint of one session with MEMORY USAGE sess:<id> — it is larger than the data you stored, because Redis charges a per-key overhead of a few hundred bytes — multiply by peak concurrent sessions, add the per-user index sets, then double the result for headroom.
# Measure, then configure.
redis-cli MEMORY USAGE sess:2Uu9k... # bytes for one session, overhead included
redis-cli CONFIG SET maxmemory 4gb
redis-cli CONFIG SET maxmemory-policy volatile-lru
redis-cli INFO stats | grep evicted_keys # must stay at 0
Peak concurrent sessions is not your user count: it is roughly daily active users multiplied by the share of the absolute session lifetime during which a typical user is active. For a product with a twelve-hour cap and users who work a couple of hours a day, that is a small fraction of the total — but measure it rather than reasoning about it, because the number is easy to get wrong by an order of magnitude in either direction.
Idle and Absolute Expiry Are Different Mechanisms
The key’s TTL implements the idle timeout: refresh it on activity and the session ends when the user stops. The absolute cap cannot be implemented that way, because refreshing the TTL is exactly what would defeat it. Store the absolute expiry as a field inside the record and check it on every read.
Refresh the TTL only when the remaining life has fallen below a threshold — half the idle window works well — rather than on every request. A busy page issuing twenty parallel calls otherwise produces twenty writes for no benefit, turning a read-mostly workload into a write-heavy one and adding load to the replica stream.
Verifying the Configuration Holds
Configuration set at runtime with CONFIG SET disappears on restart unless it is also written to the configuration file, which is a classic way for a carefully tuned instance to revert quietly. Confirm both, and add an assertion to your deployment checks that reads back maxmemory and maxmemory-policy from the running instance.
A steadily climbing key count with flat active users almost always means something is creating sessions and never ending them: a health check that authenticates, a crawler hitting the login endpoint, or a client retrying a failed login in a loop. Each is trivial to fix once visible and nearly impossible to diagnose from a memory graph alone.
Frequently Asked Questions
Why are users logged out even though the TTL has not elapsed?
Almost certainly eviction. Check evicted_keys in INFO stats: on a session store it should be zero, and any other value means Redis is discarding keys to stay under maxmemory. The second possibility is a restart without persistence, which clears everything at once rather than gradually — that shows up as a single cliff in your session count rather than a steady trickle.
Should sliding expiry refresh on every request?
No — refresh only when the remaining life drops below a threshold such as half the idle window. Refreshing on every request turns a read-mostly workload into a write-heavy one, multiplies replication traffic, and buys nothing: a user active in the last minute is equally active whether or not you rewrote the TTL for each of their twenty parallel calls.
How much memory should I allocate?
Measure one session with MEMORY USAGE, multiply by peak concurrent sessions, add the per-user index sets, and double it. The doubling is not waste — it is the difference between a traffic spike and eviction of live sessions. Then alert at 60 per cent so growth is a planning conversation rather than an incident.
Is it safe to store other data in the same instance?
It is safe only if everything in the instance can tolerate eviction, which sessions cannot. Mixing a cache — where eviction is the intended behaviour — with a session store creates a memory competition your credentials will eventually lose. Use a separate instance or at least a separate logical database with its own limit, so a cache filling up cannot sign your users out.
What happens to the per-user index when sessions expire?
Nothing — the set keeps identifiers pointing at keys that no longer exist, because expiry does not notify anything. Prune members when you read the set, cap its size per user, and give the set its own TTL matched to the absolute session lifetime. Otherwise the index grows without bound for long-lived accounts, and “sign out everywhere” starts iterating over thousands of dead identifiers.
Keeping the Record Small
Memory pressure is usually a record-size problem before it is a user-count problem, and session records grow the way log lines grow: one field at a time, each individually defensible. A record holding a user identifier, a device reference, two timestamps and a version counter fits comfortably in a couple of hundred bytes. One that also caches the user’s display name, their permission set, their feature flags and their last search query can easily be ten times that, which multiplies straight through your peak concurrency.
Beyond memory, large records cause two subtler problems. They serialize and deserialize on every request, turning a lookup that should be negligible into measurable CPU. And cached data goes stale: permissions copied into the session at login are wrong the moment an administrator changes them, producing the classic complaint that a permission change “does not take effect until you log out”. Store the user identifier and read the current values, or store a version counter you can compare cheaply.
If a piece of data genuinely belongs in the session — an in-progress checkout, a multi-step form’s state — give it its own key with its own TTL rather than inflating the session record that every request reads. That way the hot path stays small and the bulky data expires on its own schedule.
When to Split the Instance
One instance holding sessions alongside application caches is the most common cause of eviction incidents, because the cache is designed to fill available memory and the sessions are not. A cache growing to its natural size will push sessions out under allkeys policies, and even under volatile-lru the two compete for the same budget.
Split them. A separate instance — or at minimum a separate managed database with its own memory limit — means a cache filling up is a cache problem rather than a mass logout. The operational overhead is small next to the failure it prevents, and it also lets you tune persistence differently: sessions want an append-only file, while a pure cache usually wants none at all.
Splitting also makes the metrics interpretable. When one instance holds both, a memory graph tells you nothing about which workload grew, and the first useful diagnostic step during an incident becomes an archaeology exercise across key prefixes. Separate instances give you two graphs whose meaning is obvious at a glance.
The same reasoning applies to rate-limit counters, job queues and pub/sub workloads. Each has different durability and eviction requirements, and mixing them means every workload inherits the strictest constraint and the loosest failure mode simultaneously.
Related
- Managing session state in Redis — key design, replication and the per-user index this page tunes.
- Surviving Redis failover without logging everyone out — the other cause of mass logouts.
- Preventing session fixation and hijacking — why the absolute cap matters more than the idle one.