Skip to main content

Command Palette

Search for a command to run...

What is Caching, What is Redis, and the Basics You Need to Know

Updated
9 min readView as Markdown

If you've worked on any system that talks to a database, you've probably heard someone say "just cache it." This post is about what that actually means, why Redis is usually the tool reached for, and what it can do beyond being a fast key-value store.

What is Caching, Really?

A cache is a temporary storage layer that holds a copy of data so that future requests for that data can be served faster than going back to the original, slower source.

Think about it like this: your database is a library where you have to walk to the shelf, find the book, and bring it back every time — even if you asked for the same book five minutes ago. A cache is like keeping the last 20 books you borrowed on your desk. If someone asks for one of those, you hand it over instantly instead of walking back to the library.

Why does this matter in real systems?

Imagine an e-commerce product page. Every time someone visits /product/12345, your backend might:

  1. Query the products table for name, description, images

  2. Query the inventory table for stock count

  3. Query the reviews table for ratings

  4. Compute a "recommended for you" list

If this product is popular (a flash sale item, for example), you could get 10,000 requests a second for the same data. Hitting your database 10,000 times a second for identical data is wasteful — and databases have connection limits, disk I/O limits, and CPU limits. A cache sits in front of the database and absorbs that repeated load.

Without cache:
Client → App Server → Database (every single request)

With cache:
Client → App Server → Cache (hit) → Response  [fast, ~1ms] → Cache (miss) → Database → Cache is updated → Response [slower, ~50-200ms]

Why Not Just Use a Dictionary in My App's Memory?

You could — and for a single-server toy app, an in-memory HashMap or Python dict works fine. But it breaks down quickly:

  • Multiple app servers: If you run 5 instances of your app behind a load balancer, each has its own in-memory cache. A user's session cached on Server A is invisible to Server B. You get inconsistent behavior and wasted memory (same data cached 5 times).

  • Restarts lose everything: Redeploy your app, and the in-memory cache is gone.

  • No expiry management, no eviction policy, no persistence — you'd have to build all of that yourself.

This is where a shared, external, purpose-built caching system comes in. Redis is the most popular answer to this problem.

What is Redis?

Redis (REmote DIctionary Server) is an open-source, in-memory data store. The "in-memory" part is key — data lives in RAM, not on disk, which is why Redis operations are typically sub-millisecond. It can optionally persist data to disk too (we'll cover that in Post 2), but the primary read/write path is RAM.

Redis is often introduced as "a cache," but that undersells it. It's more accurate to call it an in-memory data structure server — it supports strings, lists, sets, hashes, sorted sets, streams, and more, with atomic operations on each. Many companies use Redis as:

  • A cache (the most common use case)

  • A session store (login sessions shared across servers)

  • A rate limiter (using counters with expiry)

  • A leaderboard/ranking engine (using sorted sets)

  • A message broker / pub-sub system

  • A distributed lock manager

For this series, we'll focus mainly on the caching use case, but it's worth knowing Redis is a much bigger toolbox than "fast key-value store."

Installing Redis and Your First Commands

We'll cover full local setup in Post 2, but to get a feel for it, here's Redis running via Docker and the CLI:

docker run --name redis-demo -p 6379:6379 -d redis:7.4

# Connect using the CLI
docker exec -it redis-demo redis-cli
127.0.0.1:6379> SET user:1001:name "Aditi"
OK
127.0.0.1:6379> GET user:1001:name
"Aditi"
127.0.0.1:6379> EXPIRE user:1001:name 60
(integer) 1
127.0.0.1:6379> TTL user:1001:name
(integer) 57

Three things happened here:

  • SET stored a key-value pair

  • GET retrieved it

  • EXPIRE told Redis "delete this automatically after 60 seconds" — this is the foundation of caching: data that expires on its own

Core Data Structures (Beyond Simple Key-Value)

A lot of introductions to Redis stop at SET/GET. But the real power — and the reason Redis is used for more than caching — comes from its data structures.

1. Strings — the basic cache unit

SET product:12345:price "499"
GET product:12345:price
INCR product:12345:views          # atomic increment, e.g. view counters

Real-world use: caching a JSON-serialized API response.

SET product:12345 '{"name":"Wireless Mouse","price":499,"stock":120}' EX 300

EX 300 means "expire in 300 seconds" — set it and forget it, Redis cleans up after itself.

2. Hashes — storing objects without re-serializing the whole thing

If you store a product as a JSON string, updating just the stock count means re-serializing and rewriting the entire object. A Hash lets you update one field.

HSET product:12345 name "Wireless Mouse" price 499 stock 120
HGET product:12345 stock
HINCRBY product:12345 stock -1        # atomically decrement stock by 1
HGETALL product:12345

Real-world use: shopping cart. HSET cart:user1001 item:501 2 (2 units of item 501), and inventory-style objects where fields update independently.

3. Lists — ordered collections, good for queues/feeds

LPUSH notifications:user1001 "Your order has shipped"
LPUSH notifications:user1001 "Payment received"
LRANGE notifications:user1001 0 9     # get latest 10 notifications

Real-world use: a simple activity feed, or a lightweight job queue (LPUSH to enqueue, BRPOP to consume).

4. Sets — unique, unordered collections

SADD article:99:liked_by user1001 user1002 user1003
SISMEMBER article:99:liked_by user1001    # did user1001 like this? -> 1
SCARD article:99:liked_by                 # total likes -> 3

Real-world use: "has this user already voted / liked / viewed this?" checks — deduplication that would otherwise be an expensive DB query.

5. Sorted Sets — like Sets, but with a score, kept ordered

ZADD leaderboard 1500 "player_amit"
ZADD leaderboard 2200 "player_riya"
ZREVRANGE leaderboard 0 2 WITHSCORES   # top 3 players

Real-world use: real-time leaderboards, "most viewed articles today," priority queues, rate limiting by timestamp.

6. Streams — append-only logs

XADD orders:stream '*' order_id 5001 status "created"
XRANGE orders:stream - +

Real-world use: event sourcing, activity logs, lightweight Kafka-alternative for smaller-scale pipelines.

Caching-Specific Features: TTL and Eviction Policies

Two things make Redis specifically good at caching, as opposed to just being a fast database:

TTL (Time To Live)

Every key can have an expiry. This is caching 101 — you don't want stale data living forever.

SET session:abc123 "user_data_here" EX 1800   # expires in 30 minutes

Common real-world TTL choices:

Data type Typical TTL Why
Session tokens 15–30 min Security + memory hygiene
Product catalog data 5–15 min Balances freshness vs DB load
Exchange rates 1 min Changes frequently but not every second
Static config/flags Hours Rarely changes

Eviction Policies — what happens when memory is full

This is the part beginners often miss: Redis has a memory limit (maxmemory), and when it's hit, Redis has to decide what to remove. This is configured via maxmemory-policy:

# redis.conf or via CLI
CONFIG SET maxmemory 100mb
CONFIG SET maxmemory-policy allkeys-lru

The common policies:

Policy Behavior
noeviction Returns errors on writes once memory is full — no eviction. Dangerous default for a cache.
allkeys-lru Evicts the Least Recently Used key, regardless of TTL. Most common general-purpose caching choice.
allkeys-lfu Evicts the Least Frequently Used key. Better than LRU if some keys are accessed a lot more than others (e.g., a viral product).
volatile-lru Only evicts keys that have a TTL set, using LRU order. Keys without TTL are untouchable.
volatile-ttl Evicts keys with the shortest remaining TTL first.
allkeys-random Evicts a random key. Rarely used, but cheap.

Real-world example: Say you're caching product pages with allkeys-lru and a 500MB memory cap. During a flash sale, thousands of new product-detail-page entries flood in. Redis will automatically evict the least-recently-viewed products to make room for the ones people are actively looking at — no manual cleanup code required.

If you pick noeviction by mistake in a caching use case, your application will start getting OOM command not allowed errors on writes once memory fills up — a common production incident that's easy to avoid by choosing the right policy upfront.

Cache Hit vs Cache Miss — the Core Metric

You'll hear this terminology constantly:

  • Cache hit: the data was found in Redis, no need to hit the database.

  • Cache miss: the data wasn't in Redis (either never cached, or expired/evicted), so the app falls back to the database — and typically writes the result back into Redis for next time.

import redis
import json

r = redis.Redis(host='localhost', port=6379, decode_responses=True)

def get_product(product_id):
    cache_key = f"product:{product_id}"
    cached = r.get(cache_key)

    if cached:
        print("Cache HIT")
        return json.loads(cached)

    print("Cache MISS - querying database")
    product = query_database(product_id)          # your actual DB call
    r.set(cache_key, json.dumps(product), ex=300)  # cache for 5 minutes
    return product

A healthy cache typically aims for a hit ratio of 90%+ for frequently accessed data. If your hit ratio is low, either your TTLs are too short, your eviction policy is too aggressive for your memory size, or you're caching the wrong things (e.g., data that's rarely re-requested).

What's Next

In the next post, we'll get Redis running locally properly (Docker and native), talk about persistence (RDB and AOF — so you don't lose data on restart), and start digging into Redis's single-threaded architecture and the config options that actually move the performance needle.

More from this blog

Syntax & Soul

9 posts