log in
consulting hosting industries the daily tools about contact

Redis eviction will silently eat your sessions and you won't know why

If you're running Redis on a shared VM with maxmemory-policy set to volatile-lru, your session data is not safe. I learned this the hard way.

Running Redis as both your cache and your session store on the same instance is extremely common. It's also a quiet trap that will eventually log users out at random, corrupt job queues, or drop rate-limit counters — and it will do it in a way that leaves almost no trace in your logs.

I hit this on a Laravel application I was running for a regional e-commerce client. Users were getting logged out randomly. Not always. Not reproducibly. Just... sometimes. Support tickets started coming in. I spent an embarrassing number of hours looking at session serialization, cookie config, and load balancer stickiness before I finally ran redis-cli INFO stats and saw evicted_keys climbing.

What's actually happening

When Redis hits its maxmemory limit, it needs to free memory. The maxmemory-policy setting tells it how. volatile-lru means: evict the least-recently-used key that has a TTL set. Keys with no expiry are untouchable.

Here's the problem. In a typical Laravel setup, your cache keys get TTLs because that's the whole point of a cache. Your session keys also get TTLs because sessions expire. So from Redis's perspective, when memory pressure hits, your sessions look exactly like your cache entries. Both are volatile. Both are fair game.

Redis doesn't care that one is a cache entry for a product listing and the other is the authentication token keeping a user logged in. It just picks the LRU volatile key and drops it.

No error. No warning to the application. The key is gone. Next request that tries to read that session gets nothing, Laravel treats it as a new session, and the user is logged out.

Check what you're running right now

redis-cli CONFIG GET maxmemory
redis-cli CONFIG GET maxmemory-policy
redis-cli INFO stats | grep evicted_keys

If evicted_keys is anything other than zero and you're using Redis for sessions, you have a problem. Maybe a current one, maybe a future one.

The Laravel side of this

Here's the default Laravel Redis session config, roughly:

// config/session.php
'driver' => env('SESSION_DRIVER', 'redis'),
'lifetime' => env('SESSION_LIFETIME', 120),
'expire_on_close' => false,
'connection' => 'default',

And in config/database.php, you probably have one Redis connection doing everything:

'redis' => [
    'client' => env('REDIS_CLIENT', 'phpredis'),
    'default' => [
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD', null),
        'port' => env('REDIS_PORT', 6379),
        'database' => env('REDIS_DB', 0),
    ],
],

Everything — sessions, cache, queues — hitting database 0 on the same instance. Under memory pressure, it's all at risk.

The right fix: separate the concerns

The cleanest solution is two Redis connections pointing at different logical databases (or ideally different Redis instances entirely, but on a shared VM that's often not feasible).

// config/database.php
'redis' => [
    'client' => env('REDIS_CLIENT', 'phpredis'),

    'default' => [
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD', null),
        'port' => env('REDIS_PORT', 6379),
        'database' => 0, // cache and general use
    ],

    'sessions' => [
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD', null),
        'port' => env('REDIS_PORT', 6379),
        'database' => 1, // sessions live here
    ],
],

Then point your session config at that connection:

// config/session.php
'connection' => 'sessions',

This keeps session keys in a separate keyspace. But here's the thing — this alone does not protect you from eviction. Redis eviction is per-instance, not per-database. If the instance hits maxmemory, it will still pull volatile keys from any database.

So you also need to deal with the policy itself.

Fix the eviction policy

Your options, in order of preference:

Option 1: noeviction

Redis refuses to write new keys when memory is full and returns an error instead. Your cache writes start failing loudly, which is what you want — you'll catch it immediately rather than losing sessions silently.

redis-cli CONFIG SET maxmemory-policy noeviction

This is the right policy when Redis holds data you cannot afford to lose without knowing about it. You then handle the OOM errors in your application and alert on them. Loud failures beat silent data loss every time.

Option 2: allkeys-lru for a cache-only instance

If you're running a Redis instance that is purely a cache — nothing persistent, no sessions, no queues — then allkeys-lru is fine. It'll evict anything, but you don't care because everything is reconstructable.

Option 3: Separate instances

The real answer for any production system with meaningful traffic: one Redis for ephemeral cache, one for durable-ish data (sessions, queues, locks). Two small instances on the same VM is fine. Shared memory limit across both is fine. The point is the eviction policy for the cache instance can be allkeys-lru and the policy for the sessions instance can be noeviction.

On a managed hosting setup like ours, this is something I configure at provisioning time. It's not a lot of extra overhead and it's saved clients from exactly this bug.

Persisting the config change

One gotcha: CONFIG SET changes the running config but does not write to redis.conf. If Redis restarts, you're back to whatever the config file says.

# Check what the file says
grep maxmemory /etc/redis/redis.conf

# Or use CONFIG REWRITE to persist your runtime changes back to the file
redis-cli CONFIG REWRITE

CONFIG REWRITE is convenient but I prefer editing redis.conf directly. Less magic, easier to version control, easier to explain in a runbook.

# /etc/redis/redis.conf
maxmemory 512mb
maxmemory-policy noeviction

Then systemctl restart redis and verify with CONFIG GET maxmemory-policy.

The other silent killer: no maxmemory set at all

While I'm on this topic: if maxmemory is 0 (the default), Redis will use as much memory as the OS will give it. On a shared VM running PHP-FPM, a database, and a web server, that means Redis can quietly eat all available RAM, triggering OOM kills or swap thrashing.

Always set maxmemory to a sensible limit. I typically give it 20-30% of available RAM on a shared VM, depending on what else is running. If you're not sure what to set, start conservative and watch redis-cli INFO memory over a few days.

When I'd reach for volatile-lru anyway

I'm not saying volatile-lru is never correct. If you're building a pure caching layer and you're carefully ensuring that only cache keys get TTLs, it's a reasonable middle ground. It gives you the ability to pin certain keys by not setting a TTL on them.

But in practice, on a shared Laravel instance where the same Redis is doing cache, sessions, and queues, the set of keys with TTLs is not cleanly separated by intent. They're intermixed. And that's where volatile-lru becomes a footgun.

I also wouldn't lean on logical Redis databases as a real isolation mechanism. They share the same memory, the same eviction pool, the same config. They're namespaces, not isolation. If you need real isolation between a durable store and a disposable cache, you need separate processes.

When I'd reach for a dedicated Redis for sessions

  • Any app where login continuity matters to users or the business (basically all of them)
  • Any app using Redis for queued jobs — you cannot afford random job eviction
  • Any app using Redis for rate limiting or idempotency keys
  • Anything in healthcare or e-commerce where data integrity has regulatory or financial weight

For a plain marketing site with anonymous visitors? Sure, one Redis, volatile-lru, move on. The risk profile is different.


volatile-lru is not a bug — it does exactly what it says. The problem is that on a shared instance, "volatile" ends up meaning "everything important" and there's nothing in your application logs to tell you eviction happened. Set noeviction, separate your concerns, and make your failures loud. Silent data loss is always worse than a 500 error you can alert on.

Need help shipping something like this? Get in touch.