The first two Redis commands take about a minute to learn: SET name Alice, then GET name. The harder questions arrive when you put Redis behind a real application. How should you store a list of attendees? When does a key disappear? What happens when two people try to reserve the last ticket at the same time?

We’ll answer those questions with one running example: an online event platform that tracks attendees, page views, scores, sessions, and activity. The examples use Redis Open Source 7.x and Python 3.10+. Every key beginning with lab: belongs to the exercises.

This is the first article in a series. Next, we’ll examine RDB and AOF persistence, then look at the failures that appear under real traffic.

1. What Redis Is, and Why It Usually Feels Fast

A store of data structures in memory

Think of Redis as a place where the working data lives in RAM. Your application sends commands over the network to read or change that data. Redis understands more than strings: a Set can answer whether Alice has registered, and a Sorted Set can return the top ten scores without sending the entire leaderboard to your application. Redis data types.

flowchart LR
    U[User] --> A[Application]
    A -->|commands and replies| R[(Redis: working data in RAM)]
    A -->|orders and payments| D[(System-of-record database)]
    R -.->|RDB or AOF, if enabled| F[(Disk)]

Redis often responds quickly, but the time a user sees also includes network travel, serialization, the size of the reply, and application code. A benchmark from another machine does not promise the same latency in your system. Redis latency guidance.

Redis can be the primary database for some workloads, provided you understand their durability and recovery requirements. In our event app, orders and payments live in the business database. Redis helps with cache, sessions, leaderboards, and short-lived coordination. If Redis restarts and loses a key, we should know whether the app can rebuild it and what the user will notice.

Fast does not mean every command is cheap

Compare GET on a small value with SMEMBERS on a Set containing millions of people. The second command must produce a very large response. A long-running Lua script can also make other commands wait. “It is in RAM, so it is always fast” is an unsafe assumption.

When choosing a command, read its complexity in the command documentation and measure latency from the application. p95 and p99 reveal the slowest requests that an average can hide. Redis also uses supporting threads for some I/O and background work, so the phrase “Redis is single-threaded” needs context. Redis latency guidance.

Before adding Redis to a feature, ask three questions:

Question In our event app
Can we rebuild this data? A leaderboard derived from an event log can be rebuilt. A payment record needs a trusted source.
How fresh must it be? A score may lag by a few seconds. The number of tickets left at checkout may not.
What happens if Redis is unavailable? Read the database, return slightly old data, or reject the request in a controlled way?

These answers tell you what Redis is responsible for before you start tuning speed.

2. Run Redis and Follow the Life of a Key

If you have Docker, start a local instance:

1
docker run --rm --name redis-learning -p 127.0.0.1:6379:6379 redis:7.4

Open redis-cli from another terminal:

1
docker exec -it redis-learning redis-cli

The port is bound to your own machine. Press Ctrl+C in the first terminal to stop Redis. This container has no persistent volume; use it to learn commands, then use the persistence lab when you want to test recovery. If port 6379 is already in use, choose another host port and update the Python example below.

Create, inspect, and remove a key

1
2
3
4
5
6
SET lab:event:42:title "Redis Workshop"
GET lab:event:42:title
TYPE lab:event:42:title
EXISTS lab:event:42:title
DEL lab:event:42:title
EXISTS lab:event:42:title

A key name should tell you which app it belongs to and what it contains, for example app:env:event:42:title. Clear names help debugging, although excessively long names consume RAM when you have millions of keys. The lab: prefix separates our exercise data from other data.

On a large instance, avoid KEYS * as a way to browse everything at once. Use SCAN to walk through the keyspace in batches. Its result is not a fixed snapshot if other clients are changing keys while you scan. Redis SCAN.

TTL, expiration, and eviction are different events

1
2
3
SET lab:session:alice token-123 EX 30
TTL lab:session:alice      # seconds remaining
GET lab:session:alice      # nil after the key expires

TTL is the lifetime you set. Expiration is Redis treating the key as no longer valid once that lifetime ends. Eviction happens when Redis needs memory and removes keys according to its configured policy. A key can therefore be evicted before its TTL expires.

TTL returns -2 when the key does not exist and -1 when it exists without an expiration. A plain SET that overwrites a key also removes its old TTL unless you provide EX, PX, or KEEPTTL. Key expiration, SET, eviction.

1
2
3
SET lab:temp v1 EX 30
SET lab:temp v2
TTL lab:temp               # -1: v2 no longer expires

For sessions, caches, and short-lived locks, put the TTL in the same command that creates the key when possible. SET followed by EXPIRE leaves a gap: if your process stops between the two commands, the key has no expiration.

3. Choose a Data Type for the Question You Need to Answer

It is tempting to store everything as a JSON string. That can be fine for a small value, but changing one field means rewriting the entire JSON. Asking for the top scores means moving data back into your application. Redis data types let you perform many of those operations directly. Compare Redis data types.

Application question Type to consider Starting commands
Store a token, flag, or counter? String SET, GET, INCR
Read or change one property of an event? Hash HSET, HGET, HGETALL
Has this user registered? Set SADD, SISMEMBER, SCARD
Who is at the top of the leaderboard? Sorted Set ZADD, ZINCRBY, ZREVRANGE
Need a simple ordered queue? List LPUSH, RPOP, or a blocking command
Need acknowledgments and redelivery? Stream XADD, XREADGROUP, XACK

String: values and atomic counters

1
2
3
4
SET lab:event:42:views 0
INCR lab:event:42:views
INCR lab:event:42:views
GET lab:event:42:views        # "2"

INCR performs the increase as one Redis command. If two people view the page at once, Redis records both increases. If the app instead does GET, adds one in its own code, then calls SET, both requests can read the old number and overwrite each other’s result. Use INCR for a Redis counter. Its atomicity does not automatically synchronize that counter with another database. Redis INCR.

Hash: an entity with fields

1
2
3
4
HSET lab:event:42 title "Redis Workshop" capacity 100 status open
HGET lab:event:42 capacity
HINCRBY lab:event:42 capacity -1
HGETALL lab:event:42

A Hash lets us change capacity without rewriting the whole event. But HINCRBY capacity -1 can take ticket stock below zero. For real tickets, the check and decrease must happen together, and the result must agree with the system of record. We will revisit that kind of concurrency problem in the production article. Also avoid putting every event into one enormous Hash: a huge key is harder to read, delete, or move.

Set: unique membership

1
2
3
4
SADD lab:event:42:attendees alice bob
SADD lab:event:42:attendees alice   # returns 0: already present
SISMEMBER lab:event:42:attendees alice
SCARD lab:event:42:attendees

A Set answers “has Alice registered?” nicely and removes duplicates for you. It does not keep registration order, and Alice cannot appear twice. If the set grows large, SMEMBERS returns a large response; you may only need a membership check or a partial scan. Decide when old events and their attendee sets should be removed.

Sorted Set: scores and ranks

1
2
3
4
ZADD lab:event:42:score 15 alice 10 bob
ZINCRBY lab:event:42:score 7 bob
ZREVRANGE lab:event:42:score 0 9 WITHSCORES
ZREVRANK lab:event:42:score bob

A Sorted Set holds each member once and orders members by score. It is useful for leaderboards, priority lists, and some rate-limiting algorithms. Define what a tied score means for your product; do not assume insertion order decides the tie. Redis Sorted Sets.

List, Pub/Sub, and Stream solve different delivery problems

A List can hold simple ordered work, but your application must design retries and acknowledgments. Pub/Sub behaves more like a live broadcast: subscribers that are connected receive the message; a disconnected subscriber misses it. A Stream retains entries and can track work delivered to a consumer group but not yet acknowledged with XACK. A worker may receive the same entry again, so its processing must tolerate duplicates. Choose based on delivery requirements, not only on which API is shorter. Redis Pub/Sub, Streams.

1
2
XADD lab:event:42:activity * user alice action joined
XRANGE lab:event:42:activity - + COUNT 10

XADD does not set a retention limit automatically. If you keep a Stream for a long time, decide when to trim it and what happens to slow consumers. Trimming too early can remove an entry’s payload while it is still pending. Redis XTRIM.

4. What “Atomic” Means When Requests Run Together

INCR is one command, and Redis handles that command as a unit. GET followed by SET is two commands: another client can change the key in between. Likewise, writing paid in Redis and updating an order in PostgreSQL does not create a transaction across both systems.

Pipeline saves network trips, not race conditions

Suppose you need to write 1,000 independent keys. Sending a command and waiting for a reply 1,000 times costs many network round trips. A pipeline sends multiple commands before collecting the replies. That usually helps throughput, but it does not make a GET → calculate → SET sequence atomic. The calculation still depends on a reply. Keep batches reasonably sized because Redis must buffer replies before your client reads them. Redis pipelining.

Transactions, WATCH, and Lua have different jobs

Tool Reach for it when Easy mistake
MULTI/EXEC You need Redis to execute a group of commands without another client’s command in between An error during execution does not roll back commands already run.
WATCH + MULTI/EXEC You read first and want to write only if a watched key has not changed Competing clients can force retries.
Lua script You need to read, decide, and write in one call to Redis A long script blocks other work; it is not a transaction with an external database.

For example, a plain DECR on ticket stock can go below zero. A short Lua script could check that stock is above zero and then decrease it in one execution. Payment and the business database still live outside Redis, however. You still need rules for reservations, timeouts, and refunds. Redis transactions, Lua scripting.

5. Four Common Uses and Their Limits

Sessions. SET session:<random-id> <user-id> EX 1800 gives a session a 30-minute lifetime. Generate an unpredictable ID; keep sensitive information out of key names and logs. Delete the key on logout. If Redis restarts without that session, the user may have to log in again. Some products accept this; others need more durable session storage.

Cache-aside. The application checks Redis first. On a miss it reads the database and fills Redis:

sequenceDiagram
    participant U as User
    participant A as Application
    participant R as Redis
    participant D as Database
    U->>A: View event 42
    A->>R: GET cache:event:42
    alt cache hit
        R-->>A: Cached event
    else cache miss
        A->>D: SELECT event 42
        D-->>A: Event
        A->>R: SET cache:event:42 ... EX 60
    end
    A-->>U: Response

This reduces database reads, but two problems remain: an update can race with a cache fill, and many requests can miss the same popular key together. The production article works through both. Redis cache-aside.

Leaderboards. ZINCRBY adds points and ZREVRANGE reads the top N. A recreational leaderboard that can be rebuilt from event history fits Redis well. If points determine a cash prize, keep an auditable source of changes; a Sorted Set in RAM should not be the only record.

Notifications and work. Pub/Sub can tell online viewers that Alice joined. A Stream is a better candidate when a worker must send an email even after restarting. XACK tells Redis that the worker acknowledged an entry; it does not prove that an external email was sent exactly once. Redis Pub/Sub, Streams.

6. Memory, Cluster, Security, and Observability

Decide what happens when RAM fills up

Redis uses RAM, so maxmemory and its eviction policy are product decisions. For a rebuildable cache, allkeys-lru or allkeys-lfu may be worth testing. With noeviction, some writes fail when Redis reaches the limit. Be careful if the same instance also holds sessions or important coordination keys: losing one of those can change application behavior. Monitor used_memory, evicted_keys, and the load on your database after a cold cache starts filling again. Redis eviction, INFO.

Replicas, Cluster, and persistence answer different questions

Mechanism Main purpose What it does not guarantee
Replica Another copy for reads and failover The newest write may not have reached the promoted replica.
Cluster Distribute keys across machines One extremely popular key still belongs to one slot; multi-key commands have slot constraints.
RDB/AOF Reload data from disk after restart A separate backup or the survival of every acknowledged write.

Redis Cluster assigns keys to hash slots. Keys used together by a multi-key command or script generally need the same slot. Hash tags can do that: event:{42}:stock and event:{42}:version share {42}. Do not tag every event {event}, or they will all land in one slot. Redis Cluster specification.

A useful GitLab incident: long TTLs and eviction spikes

In GitLab’s analysis of Redis latency spikes, slow periods coincided with bursts of eviction. Evicting many keys consumed CPU; waiting requests added memory pressure and made the episode worse. The team followed metrics from the client through CPU and RAM, then reduced the default cache TTL from two weeks to eight hours. In their workload, RAM usage fell without a significant drop in cache hit rate.

The lesson is to look beyond a pleasing hit-rate number. Rarely read keys still consume RAM, and continuous eviction can make users wait. Measure hit rate alongside p99 latency, eviction rate, and memory. Eight hours was GitLab’s result for that workload, not a universal Redis TTL.

Measure and protect the system people actually use

Measure p95/p99 from the application, connection and timeout counts, memory, evictions, keyspace_hits/keyspace_misses, and database load behind the cache. A good overall hit rate can hide one popular key expiring and overwhelming the database for a few seconds. Track the event cache separately from sessions where possible. Redis INFO.

Do not expose Redis directly to the Internet. Limit network access to services that need it, use ACLs to restrict commands and keys, and keep credentials out of code and blog posts. The password-free local CLI in this article is only for an isolated learning environment. Redis security, ACLs.

7. A Small Python Lab

Keep the Redis container from section 2 running. Install the client with python -m pip install redis, save the following as redis_fundamentals_demo.py, and run python redis_fundamentals_demo.py. The script deletes four exercise keys under lab:event:{42}:, so use an instance dedicated to learning.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import redis

r = redis.Redis(host="localhost", port=6379, decode_responses=True)
event = "lab:event:{42}"
meta = f"{event}:meta"
attendees = f"{event}:attendees"
score = f"{event}:score"
views = f"{event}:views"

r.delete(meta, attendees, score, views)
r.hset(meta, mapping={"title": "Redis Workshop", "capacity": 100})
r.sadd(attendees, "alice", "bob")
r.zadd(score, {"alice": 10, "bob": 15})
r.set(views, 0, ex=60)

r.incr(views)
r.zincrby(score, 8, "alice")

print("Event:", r.hgetall(meta))
print("Is Alice registered?", bool(r.sismember(attendees, "alice")))
print("Attendee count:", r.scard(attendees))
print("Top scores:", r.zrevrange(score, 0, 1, withscores=True))
print("Views:", r.get(views), "TTL:", r.ttl(views))

You should see one view. Alice has 18 points and ranks above Bob, who has 15. The views key has a positive TTL; the other three keys do not. That difference is deliberate: even keys belonging to the same event have separate lifetimes.

Try changing the example:

  1. Run SET lab:event:{42}:views 99 and then TTL lab:event:{42}:views. Why is the result -1?
  2. Add alice to the attendee Set again. Does SCARD grow? What if the product needs to record two separate registrations?
  3. Would you accept losing page views after a restart? What about the attendee list? Answer from product requirements, not from a preference for Redis.
  4. Ask ZREVRANGE for the top ten. Does the application need to download and sort the entire Sorted Set?

8. Where to Go Next

When I decide whether to use Redis for a feature, I walk through four questions: how will the app read and change this data, how long should a key live, what if several requests touch it together, and what happens when Redis is unavailable? Those answers lead to the data type, TTL, and recovery plan.

The next article explains how RDB and AOF keep data across restarts. The final article examines cache races, traffic spikes, rate limits, and less obvious production failures.

Further reading