Redis started as a cache. A place to dump serialized data so your database does not melt under load. But Redis evolved. It is now a data structure server, a message broker, a rate limiter, a session store, and a real-time analytics engine. Understanding Redis means understanding how modern applications handle state at scale.
Redis is not just a cache. It is a Swiss Army knife for application state.
The cache-aside pattern is the foundation. Your application checks Redis first. If the data exists, it returns immediately. If not, it queries the database, writes the result to Redis with a TTL, and returns it. Simple, effective, and the reason most sites stay fast under traffic spikes.
<?php
declare(strict_types=1);

class PostRepository
{
    private Redis $redis;
    private PDO $db;
    private int $ttl = 3600; // 1 hour

    public function findById(int $id): ?Post
    {
        $cacheKey = "post:{$id}";

        // 1. Check cache
        $cached = $this->redis->get($cacheKey);
        if ($cached !== false) {
            return Post::fromJson($cached);
        }

        // 2. Query database
        $stmt = $this->db->prepare('SELECT * FROM posts WHERE id = ?');
        $stmt->execute([$id]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$row) {
            return null;
        }

        $post = Post::fromArray($row);

        // 3. Write to cache with TTL
        $this->redis->setex($cacheKey, $this->ttl, json_encode($post));

        return $post;
    }
}
But caching has a dark side. Cache stampede happens when a hot key expires and thousands of concurrent requests simultaneously hit the database. The solution is probabilistic early expiration. Instead of letting the key expire naturally, you regenerate it before expiration with a small probability that increases as the TTL approaches zero.
<?php
// Probabilistic early expiration prevents stampede
function getWithEarlyExpiration(
    Redis $redis,
    string $key,
    callable $fetch,
    int $ttl = 3600,
    float $beta = 1.0
): mixed {
    $value = $redis->get($key);
    $ttlRemaining = $redis->ttl($key);

    if ($value === false || $ttlRemaining < 0) {
        // Key missing or expired — regenerate
        $value = $fetch();
        $redis->setex($key, $ttl, serialize($value));
        return $value;
    }

    // Probability increases as TTL runs out
    $probability = exp(-$beta * $ttlRemaining / $ttl);

    if (random_int(1, 100) <= $probability * 100) {
        // Regenerate in background (async or queue)
        // Return stale value immediately
        asyncRegenerate($key, $fetch, $ttl);
    }

    return unserialize($value);
}
Redis Streams are the modern alternative to Pub/Sub for event-driven architectures. Unlike Pub/Sub, which drops messages if no subscriber is listening, Streams persist events. Consumers read from the stream, acknowledge processing, and Redis tracks exactly where each consumer group left off. It is Kafka without the operational overhead.
Rate limiting with Redis is elegant. A sorted set stores timestamps. Each request adds a new member and trims the set to your window size. If the cardinality exceeds your limit, you reject the request. This sliding window algorithm is accurate, stateless across application servers, and executes in a single Lua script for atomicity.
-- Atomic sliding window rate limiter
local key = KEYS[1]
local window = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

-- Remove entries outside the window
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)

-- Count remaining entries
local count = redis.call('ZCARD', key)

if count < limit then
    -- Allow request, record timestamp
    redis.call('ZADD', key, now, now .. ':' .. ARGV[4])
    redis.call('EXPIRE', key, window)
    return {1, limit - count - 1}
else
    -- Reject request
    return {0, 0}
end
Redis is not magic. It is memory, and memory is finite. Plan for eviction. Monitor memory usage. Use appropriate maxmemory policies. But when used correctly, Redis transforms application performance. The difference between a database query and a Redis lookup is the difference between 50 milliseconds and 500 microseconds. That is two orders of magnitude. That is the difference between a site that scales and one that collapses.