DEV Community

Cover image for Eviction Policy: Where Redis Decides Who Gets Kicked Out 🚪
Igor Nosatov
Igor Nosatov

Posted on

Eviction Policy: Where Redis Decides Who Gets Kicked Out 🚪

Redis Eviction Policy in Laravel: Strategy and Implementation

Introduction

Redis is an in-memory data store with finite memory capacity. When Redis reaches its maximum memory limit, it must decide which keys to remove to make space for new data. This process is called eviction, and understanding eviction policies is crucial for building reliable Laravel applications that depend on Redis for caching, sessions, and queuing.

This article explores Redis eviction policies, their patterns, strategies for implementation in Laravel, and best practices for production environments.

What is Redis Eviction Policy?

An eviction policy is a set of rules that determines which keys Redis should delete when it runs out of memory. Without an eviction policy, Redis would return "out of memory" errors and fail to store new data. The eviction policy ensures graceful degradation and continuous operation under memory pressure.

Redis Eviction Policy Types

Redis provides eight built-in eviction policies, grouped into three categories:

Expiration-Based Policies

These policies only evict keys that have a time-to-live (TTL) set:

  1. volatile-lru: Evicts least recently used keys among those with TTL
  2. volatile-lfu: Evicts least frequently used keys among those with TTL
  3. volatile-ttl: Evicts keys with TTL, prioritizing those closest to expiration
  4. volatile-random: Randomly evicts keys with TTL

Evict All Policies

These policies consider all keys, regardless of TTL:

  1. allkeys-lru: Evicts least recently used keys (most common choice)
  2. allkeys-lfu: Evicts least frequently used keys
  3. allkeys-random: Randomly evicts any key

No Eviction

  1. noeviction: Disables eviction entirely (default); Redis returns errors when memory is full

Understanding the Patterns

LRU (Least Recently Used)

LRU eviction tracks the last access time of each key. Keys accessed longest ago are evicted first. This works well when recently accessed data is more likely to be needed again.

Scenario: A Laravel application caching user session data. If a user hasn't been active in hours, their session data is less valuable and can be evicted.

LFU (Least Frequently Used)

LFU tracks how often each key is accessed. Keys used least frequently are evicted first. This is superior for workloads where frequently-accessed data should persist.

Scenario: A content delivery system where popular articles are cached and infrequently-viewed content can be cleared.

TTL-Based

This strategy prioritizes evicting data closest to its expiration time, minimizing waste of expired data consuming memory.

Scenario: Cache entries with varying TTLs; those about to expire anyway are evicted first.

Configuring Eviction Policy in Redis

The eviction policy is set in the Redis configuration file (redis.conf) or via runtime commands:

# In redis.conf
maxmemory 256mb
maxmemory-policy allkeys-lru
Enter fullscreen mode Exit fullscreen mode

Or at runtime:

CONFIG SET maxmemory 256000000
CONFIG SET maxmemory-policy allkeys-lru
Enter fullscreen mode Exit fullscreen mode

View current settings:

CONFIG GET maxmemory
CONFIG GET maxmemory-policy
Enter fullscreen mode Exit fullscreen mode

Implementing Eviction Policy Strategy in Laravel

1. Basic Cache Configuration with Eviction Strategy

Set up your Laravel cache to work with Redis and an appropriate eviction policy:

// config/cache.php
'redis' => [
    'driver' => 'redis',
    'connection' => 'default',
    'ttl' => 3600, // 1 hour default TTL
],

// config/database.php
'redis' => [
    'default' => [
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD', null),
        'port' => env('REDIS_PORT', 6379),
        'database' => 0,
    ],

    'cache' => [
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD', null),
        'port' => env('REDIS_PORT', 6379),
        'database' => 1, // Separate database for cache
    ],
],
Enter fullscreen mode Exit fullscreen mode

2. Implementing LRU Strategy in Laravel

<?php

namespace App\Services;

use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Cache;

class CacheStrategy
{
    /**
     * Store a value with LRU-friendly TTL
     * Shorter TTLs encourage LRU to work effectively
     */
    public function storeCacheablData($key, $value, $ttl = 3600)
    {
        Cache::put($key, $value, $ttl);
    }

    /**
     * Get cache stats and memory usage
     */
    public function getCacheStats()
    {
        $info = Redis::command('INFO', ['memory']);

        return [
            'memory_used' => $info['used_memory_human'],
            'memory_peak' => $info['used_memory_peak_human'],
            'memory_percent' => ($info['used_memory'] / $info['maxmemory']) * 100,
            'evicted_keys' => $info['evicted_keys'] ?? 0,
        ];
    }

    /**
     * Check eviction rate (indicates memory pressure)
     */
    public function getEvictionRate()
    {
        $stats = Redis::command('INFO', ['stats']);

        return [
            'evicted_keys' => $stats['evicted_keys'] ?? 0,
            'keyspace_hits' => $stats['keyspace_hits'],
            'keyspace_misses' => $stats['keyspace_misses'],
            'hit_rate' => $stats['keyspace_hits'] / 
                         ($stats['keyspace_hits'] + $stats['keyspace_misses']) * 100,
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Tiered Caching with Different TTLs

Implement a strategy where different data types have different TTLs based on volatility:

<?php

namespace App\Services;

use Illuminate\Support\Facades\Cache;

class TieredCacheStrategy
{
    // Short-lived: Frequently changing data
    const SHORT_TTL = 300; // 5 minutes

    // Medium-lived: Semi-stable data
    const MEDIUM_TTL = 3600; // 1 hour

    // Long-lived: Stable data
    const LONG_TTL = 86400; // 1 day

    /**
     * Cache user session (frequently changing)
     */
    public function cacheUserSession($userId, $sessionData)
    {
        $key = "user_session:{$userId}";
        Cache::put($key, $sessionData, self::SHORT_TTL);
    }

    /**
     * Cache user profile (semi-stable)
     */
    public function cacheUserProfile($userId, $profileData)
    {
        $key = "user_profile:{$userId}";
        Cache::put($key, $profileData, self::MEDIUM_TTL);
    }

    /**
     * Cache configuration (stable)
     */
    public function cacheConfig($configKey, $configData)
    {
        $key = "config:{$configKey}";
        Cache::put($key, $configData, self::LONG_TTL);
    }

    /**
     * Cache with adaptive TTL based on access frequency
     */
    public function cacheWithAdaptiveTTL($key, $value, $accessCount = 0)
    {
        // More frequently accessed items get longer TTL
        if ($accessCount > 100) {
            $ttl = self::LONG_TTL;
        } elseif ($accessCount > 50) {
            $ttl = self::MEDIUM_TTL;
        } else {
            $ttl = self::SHORT_TTL;
        }

        Cache::put($key, $value, $ttl);
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Monitoring and Reacting to Eviction

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Log;

class MonitorRedisEviction extends Command
{
    protected $signature = 'redis:monitor-eviction';
    protected $description = 'Monitor Redis eviction rate and alert on high eviction';

    public function handle()
    {
        $previousEvicted = 0;
        $threshold = 100; // Alert if more than 100 keys evicted per check

        while (true) {
            try {
                $info = Redis::command('INFO', ['stats']);
                $currentEvicted = $info['evicted_keys'] ?? 0;

                $evictedSinceLastCheck = $currentEvicted - $previousEvicted;

                // Get memory stats
                $memory = Redis::command('INFO', ['memory']);
                $memoryPercent = ($memory['used_memory'] / $memory['maxmemory']) * 100;

                if ($evictedSinceLastCheck > $threshold) {
                    Log::warning('High Redis eviction detected', [
                        'evicted_since_last_check' => $evictedSinceLastCheck,
                        'total_evicted' => $currentEvicted,
                        'memory_usage_percent' => round($memoryPercent, 2),
                        'memory_used' => $memory['used_memory_human'],
                    ]);

                    $this->warn("⚠️  High eviction: {$evictedSinceLastCheck} keys evicted");
                    $this->warn("Memory usage: {$memoryPercent}%");
                } else {
                    $this->info("✓ Eviction normal: {$evictedSinceLastCheck} keys (Memory: {$memoryPercent}%)");
                }

                $previousEvicted = $currentEvicted;
                sleep(60); // Check every minute

            } catch (\Exception $e) {
                Log::error('Redis monitoring error: ' . $e->getMessage());
                $this->error('Error: ' . $e->getMessage());
                sleep(30);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Graceful Degradation When Memory Pressure Occurs

<?php

namespace App\Services;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

class CacheWithFallback
{
    /**
     * Try to get from cache, fall back to computation if evicted
     */
    public function getOrCompute($key, callable $computation, $ttl = 3600)
    {
        try {
            // Try to get from cache
            $cached = Cache::get($key);
            if ($cached !== null) {
                return $cached;
            }

            // Compute fresh data
            $result = $computation();

            // Try to cache, but don't fail if memory is full
            try {
                Cache::put($key, $result, $ttl);
            } catch (\Exception $e) {
                Log::warning("Failed to cache {$key}: {$e->getMessage()}");
                // Continue anyway - we have the result
            }

            return $result;

        } catch (\Exception $e) {
            Log::error("Cache computation failed for {$key}: {$e->getMessage()}");
            return null;
        }
    }

    /**
     * Check memory pressure before caching large items
     */
    public function getMemoryPressure()
    {
        try {
            $info = Redis::command('INFO', ['memory']);
            $used = $info['used_memory'];
            $max = $info['maxmemory'];

            if ($max === 0) return 'unlimited'; // No memory limit

            $percent = ($used / $max) * 100;

            if ($percent < 70) return 'low';
            if ($percent < 85) return 'medium';
            if ($percent < 95) return 'high';
            return 'critical';

        } catch (\Exception $e) {
            return 'unknown';
        }
    }

    /**
     * Store data only if memory pressure is acceptable
     */
    public function cacheIfNotUnderPressure($key, $value, $ttl = 3600)
    {
        $pressure = $this->getMemoryPressure();

        if ($pressure === 'critical') {
            Log::info("Skipping cache storage for {$key} due to critical memory pressure");
            return false;
        }

        Cache::put($key, $value, $ttl);
        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

Recommended Strategy for Laravel Applications

Best Practice Eviction Policy

For most Laravel applications, allkeys-lru is recommended because:

  • It evicts any key, not just those with TTL
  • It's predictable and well-understood
  • It keeps your most-used data in memory
  • It's computationally efficient

Alternative: Use allkeys-lfu if you have workloads where some data is accessed far more frequently than others.

Implementation Checklist

  1. Set maxmemory appropriately
   maxmemory 2gb
   maxmemory-policy allkeys-lru
Enter fullscreen mode Exit fullscreen mode
  1. Use meaningful TTLs in Laravel
   Cache::put('key', $value, 3600); // Set explicit TTL
Enter fullscreen mode Exit fullscreen mode
  1. Monitor eviction metrics

    • Track evicted_keys from Redis INFO stats
    • Alert if eviction rate exceeds threshold
    • Adjust maxmemory if constantly evicting
  2. Implement separate Redis databases

    • Use database 0 for sessions
    • Use database 1 for cache
    • Use database 2 for queues
  3. Test under load

    • Simulate memory pressure
    • Verify application behavior during eviction
    • Ensure critical data has appropriate TTLs

Conclusion

Redis eviction policies are essential for production Laravel applications. By understanding different eviction strategies, configuring appropriate policies, and implementing monitoring, you can ensure your Redis infrastructure remains stable and performant under memory constraints. The key is choosing a strategy aligned with your application's access patterns and consistently monitoring eviction rates to detect problems before they impact users.

Top comments (0)