Right, let’s talk about using Redis as a cache. Because if you’re hitting your primary database for every single request for “user 123’s profile pic URL,” you’re not just wasting money, you’re actively choosing to live in a world of pain. A cache is a high-speed data storage layer that lets you serve copies of frequently accessed data, lightning fast. And Redis, being an in-memory data structure store, is so stupidly fast for this job it’s almost unfair to the other databases.

But here’s the thing you need to get into your head immediately: Redis is a cache, not your primary database. It’s ephemeral. It can, and it will, lose your data. It might get restarted, it might run out of memory and evict things, it might just feel a bit peevish that day. If the data is precious, it belongs in your persistent store (PostgreSQL, MongoDB, etc.). Redis is just a charismatic, incredibly quick liar you keep in front of it to take the heat off.

The Almighty EXPIRE (and Its Siblings)

The most fundamental concept in caching is expiry. You don’t want user session data from 2012 just lounging around in memory, do you? Redis gives you several tools to handle this automatically.

The workhorse is the EXPIRE command. You set a key, and then you tell it how many seconds it has to live. It’s a death timer, and it’s brilliantly simple.

# Set a key to hold the string "temporary_value"
SET user:456:profile "{"name": "Alice", "role": "admin"}"
# Command it to die in 300 seconds (5 minutes)
EXPIRE user:456:profile 300

You can do it in one line with SETEX, which is just SET and EXPIRE with fewer round trips, because we’re not savages.

SETEX user:456:profile 300 "{"name": "Alice", "role": "admin"}"

Now, what if you want to check on your condemned key? TTL (Time To Live) returns the number of seconds until it gets vaporized. It returns -2 if the key doesn’t exist (so, it’s already gone), and -1 if the key exists but has no expiry set (a potential pitfall!).

TTL user:456:profile
> (integer) 294  # (output 6 seconds later)
TTL user:456:profile
> (integer) 288

A more subtle command is EXPIREAT, which doesn’t take a duration; it takes a UNIX timestamp (seconds since the epoch) for when it should die. Useful for making something expire at midnight, for example.

How Redis Decides What to Kill (The LRU Gauntlet)

So, you’ve set a bunch of expiries, but what happens when Redis hits its memory limit? It can’t just crash. This is where eviction policies come in. You configure this in the redis.conf file, and the most common one for a cache is allkeys-lru (Least Recently Used).

LRU is brilliantly simple in theory: when Redis needs to free up memory, it looks for the key that was accessed the longest time ago and boots it out. The idea is that if you haven’t used it recently, you probably don’t need it. In practice, Redis uses an approximated LRU for performance reasons, meaning it samples a handful of keys and evicts the oldest from that sample. It’s not perfect, but it’s very efficient and “perfect enough” for caching.

Other policies exist, like volatile-lru (only evict keys with an expiry set), allkeys-random (chaos mode), or noeviction (which just stops accepting new writes—terrible for a cache, good for a temporary data store where you’d rather have errors than data loss). For a pure cache, allkeys-lru is almost always your best bet.

The Cache-Aside Pattern: Do It, Don’t Overthink It

This isn’t a Redis feature, it’s an application design pattern. And it’s so simple, people often look for something more complicated. Don’t. Here’s the flow:

  1. Your application needs some data (e.g., user:123).
  2. It first checks the cache (GET user:123).
    • Cache Hit: If it’s in Redis, glorious! Use the data and move on with your life.
    • Cache Miss: If it’s not in Redis (it expired or was evicted), you…
  3. …go to your real, persistent database and get the data the hard way.
  4. You populate the cache (SETEX user:123 3600 {data}) so the next request is faster.
  5. You return the data to the application.

Here’s what that looks like in pseudo-code:

def get_user(user_id):
    # Try the cache first
    cache_key = f"user:{user_id}"
    data = redis.get(cache_key)

    if data is not None:
        # Cache Hit! Deserialize and return.
        return json.loads(data)

    # Cache Miss. Sad trombone. Go to the source of truth.
    user_data = db.query("SELECT * FROM users WHERE id = %s", user_id)
    if user_data:
        # Populate the cache for next time, expire in 1 hour.
        redis.setex(cache_key, 3600, json.dumps(user_data))

    return user_data

The beauty of Cache-Aside is its resilience. If Redis completely melts down, your application just degrades to hitting the database every time. It’s slow, but it doesn’t break. The flip side is the potential for stale data—if you update the user in the database, you need to invalidate or update the cache (DEL user:123), otherwise the old data will sit there, lying to everyone for up to an hour. This is the dual-write problem, and it’s the price of admission for this simple pattern. For many use cases, a short expiry is a good enough solution.