Redis Introduction
Redis is an open-source, in-memory data structure store that acts as a database, cache, and message broker all at once. Because it keeps its entire dataset in RAM rather than on disk, most Redis operations complete in well under a millisecond, which is why it sits in the critical path of so many high-traffic applications — as a cache in front of a slower database, as a session store, as a counter or rate limiter, or as the backbone of a real-time leaderboard. Unlike a plain key-value cache, Redis values can be rich data structures — strings, hashes, lists, sets, sorted sets, and streams — each with its own commands built for a specific access pattern. This lesson covers what Redis is, how it works internally, and the core concepts every later lesson in this course builds on.
Overview: What Redis Is and How It Works
Redis (REmote DIctionary Server) is fundamentally a key-value store: every piece of data is addressed by a unique key — a binary-safe string, though in practice almost always plain text — mapped to a value. What sets Redis apart from a basic cache like Memcached is that the value is not limited to a string. It can be a list, a hash (a map of field-value pairs), a set, a sorted set, a bitmap, a HyperLogLog, or a stream, and Redis ships dozens of commands that operate directly on those structures on the server. An operation like "add this item to a list" or "increment a field in a hash" happens entirely inside Redis — you don’t fetch the value, modify it in your application, and write it back.
Single-Threaded Execution and Atomicity
Redis processes commands one at a time on a single main thread. Only one command runs at any instant, so a command can never be interrupted halfway by another client’s command — every individual command is atomic by construction. This is why Redis is safe for counters (INCR), simple locks, and rate limiters without any extra application-level locking: two clients calling INCR pageviews:home at the same moment can never both read the same starting value and stomp on each other, because the second INCR simply cannot begin until the first finishes. (Modern Redis uses background threads for slow deletes and network I/O, but the execution of your commands still happens one at a time on the main thread.)
Persistence: RDB and AOF
Because Redis lives in memory, it needs a way to survive a restart. It offers two persistence mechanisms, and you can use either or both:
- RDB (Redis Database) — periodic point-in-time snapshots of the whole dataset written to disk. Compact and fast to restore, but anything written since the last snapshot is lost on a crash.
- AOF (Append Only File) — a log of every write command, appended as it happens. Far more durable, but the file is larger and slower to replay on restart.
Neither is strictly better — many production deployments enable both, using AOF for durability and periodic RDB snapshots for fast backups and replication bootstrapping.
How Expiration Actually Works
A key with a TTL (time-to-live) isn’t necessarily deleted the instant it expires — Redis uses two complementary strategies. Lazy expiration checks a key’s TTL whenever it’s accessed; if it’s past its expiry, Redis deletes it on the spot and behaves as if it never existed. Active expiration runs in the background: several times a second Redis samples a small set of keys that have a TTL, deletes any that have expired, and repeats if the sample had a lot of expired keys in it. Together these keep memory from filling up with dead keys nobody reads again, without requiring a full scan of the keyspace.
When to Reach for Redis (and When Not To)
Redis excels at caching, session storage, counters and rate limiters, leaderboards (sorted sets), pub/sub messaging, and lightweight job queues (lists or streams). It is not a drop-in replacement for a relational database — it has no query language for ad hoc joins or filtering across arbitrary fields, and while it can persist to disk, it’s built around everything fitting in RAM. Use it alongside your primary datastore for the pieces that need to be fast, not as a wholesale substitute for one.
Syntax
Every Redis command has the same shape at the redis-cli prompt: the command name, then the key it operates on, then any further arguments specific to that command. Redis is case-insensitive about command names, but the convention on this site is UPPERCASE for commands and lowercase for your own keys and values.
SET key value [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-ms] [NX|XX] [KEEPTTL] [GET]
SET— the command name.key— the key to write, using a colon-namespaced convention, e.g.user:1001:name.value— the string to store.EX seconds/PX milliseconds— set a TTL at write time, in seconds or milliseconds.EXAT/PXAT— expire at a specific Unix timestamp instead of a relative offset.NX— only set the key if it does not already exist.XX— only set the key if it already exists.KEEPTTL— keep any existing TTL instead of clearing it (a plainSETwithout this flag always clears an existing TTL).GET— return the key’s old value instead of the usualOK.
Keys are binary-safe strings up to 512 MB, though in practice keep them short and predictable. This course uses entity:id:field namespacing throughout, like user:1001:email or session:abc123, because Redis has no built-in concept of tables — a consistent naming scheme is how you keep a large keyspace organized.
Core Data Types at a Glance
| Type | Example commands | Typical time complexity | Good for |
|---|---|---|---|
| String | SET, GET, INCR |
O(1) | Caching, counters, flags |
| List | LPUSH, RPUSH, LRANGE |
O(1) push, O(N) range | Queues, recent-activity feeds |
| Hash | HSET, HGET, HGETALL |
O(1) per field, O(N) for all fields | Records (a user profile) |
| Set | SADD, SISMEMBER |
O(1) | Unique membership, tags |
| Sorted Set | ZADD, ZRANGE |
O(log N) add, O(log N + M) range | Leaderboards, priority queues |
| Stream | XADD, XRANGE |
O(1) append | Event logs, message queues |
A sorted set is not the same as a plain set: it attaches a floating-point score to every member and keeps members ordered by that score, which is exactly what a leaderboard or a "top 10" range query needs. A plain set has no ordering at all. Each type gets its own dedicated lesson later in this course — this lesson focuses on string commands and the concepts every type shares.
Examples
Example 1: Storing and Reading a Simple String
The most basic Redis interaction: write a value under a key, then read it back.
SET greeting "Hello, Redis!"
GET greeting
STRLEN greeting
DEL greeting
GET greeting
Output:
OK
"Hello, Redis!"
(integer) 13
(integer) 1
(nil)
SET replies OK on success. GET returns the stored string. STRLEN returns its length in bytes without transferring the whole value. DEL removes the key and reports how many keys it deleted (1, since it existed). After deletion, GET on the same key returns (nil) — Redis’s way of saying "this key doesn’t exist," not an error.
Example 2: TTLs, KEEPTTL, and How SET Clears Expiration
This shows the most common source of confusion around expiration: a plain SET on an existing key silently clears its TTL unless you pass KEEPTTL.
SET session:abc123 "user:42" EX 60
TTL session:abc123
SET session:abc123 "user:42-updated" KEEPTTL
TTL session:abc123
SET session:abc123 "user:42-reset"
TTL session:abc123
Output:
OK
(integer) 60
OK
(integer) 60
OK
(integer) -1
The first SET writes the key with a 60-second TTL via EX 60, and TTL confirms it. The second SET overwrites the value but adds KEEPTTL, so the countdown survives — TTL still reports 60 since almost no real time has elapsed. The third SET omits KEEPTTL, so even though it’s just overwriting the same key again, the TTL is wiped and TTL now returns -1, meaning the key exists but never expires.
Example 3: A Hash for a Record, Plus an Atomic Counter
A more realistic example: storing a small user record as a hash rather than several separate string keys, and tracking a page-view counter atomically.
HSET user:1001 name "Ada" email "ada@example.com" signup_year 2024
HGETALL user:1001
INCR pageviews:home
INCR pageviews:home
GET pageviews:home
Output:
(integer) 3
1) "name"
2) "Ada"
3) "email"
4) "ada@example.com"
5) "signup_year"
6) "2024"
(integer) 1
(integer) 2
"2"
HSET sets multiple field-value pairs in one call and returns how many new fields it created — 3, since all three were new. HGETALL returns every field and value as a flat array, alternating field name and value. INCR treats a key as an integer, creating it at 0 if it doesn’t exist and then adding 1, atomically. Note that GET on the same key returns the number as a string ("2") — Redis strings that look like integers are still stored and returned as strings; INCR just parses and re-serializes them.
How Redis Processes a Command, Step by Step
Walking through what happens when a client sends SET user:1001:name "Ada":
- The client sends the command over its connection using RESP (REdis Serialization Protocol).
- Redis’s single event loop reads the command off the socket and parses it into a name plus arguments.
- The command name is looked up in Redis’s internal command table, which validates arity and argument types before execution — a malformed call like
SETwith no value errors immediately rather than doing anything. - The implementation runs against the in-memory structures — for a string
SET, this means inserting or overwriting an entry in the main hash table backing the keyspace. Because this happens on the single main thread with nothing else running concurrently, it’s atomic with respect to every other command. - If AOF is enabled, the command is appended to the AOF buffer to be written (and optionally fsynced) to disk; a background RDB save may also trigger if configured save points have been reached.
- Redis sends the reply (
OK) back over the same connection. - Because this was a plain
SET, any TTL the key previously had is cleared, sinceSETreplaces the key’s expiration state entirely unlessKEEPTTLwas given.
Common Mistakes
1. Using KEYS in Production
KEYS pattern scans the entire keyspace to find matching keys, and because Redis is single-threaded, nothing else can be served while that scan runs — on millions of keys this can freeze every other client for seconds. Use the cursor-based SCAN instead: it walks the keyspace incrementally over multiple round trips and never blocks the server for more than a tiny slice of time.
KEYS user:*
SCAN 0 MATCH user:* COUNT 100
KEYS is fine for one-off exploration on a small development database, but it should never appear in application code that runs against production data.
2. Forgetting to Set a TTL and Leaking Memory Forever
Anything written with a plain SET has no expiration by default. If you’re using Redis as a cache and never attach a TTL, entries accumulate forever and memory climbs until Redis starts evicting keys (if a maxmemory policy is configured) or runs out of memory entirely.
SET cache:report:2024 "report-data"
TTL cache:report:2024
EXPIRE cache:report:2024 3600
TTL cache:report:2024
The first TTL call returns -1 — the key exists but never expires. Calling EXPIRE afterward attaches a one-hour TTL retroactively, and the second TTL confirms it. It’s better to attach the TTL at write time with SET ... EX 3600 so there’s never a window where the key is unexpectedly permanent.
3. Assuming EXPIRE Does Something on a Key That Doesn’t Exist
EXPIRE on a key that isn’t there doesn’t error and doesn’t create the key — it just reports that it did nothing.
EXPIRE nonexistent:key 100
This returns (integer) 0, meaning no TTL was set because the key doesn’t exist (a successful call returns (integer) 1). Always check this return value if your logic depends on the TTL actually being applied — silently ignoring it is a common source of "why does this key never expire" bugs.
4. Calling a Command on the Wrong Type
Every key has exactly one data type, fixed at creation. Calling a list command on a key that holds a string (or vice versa) doesn’t coerce anything — it errors.
SET user:1001:name "Ada"
LPUSH user:1001:name "extra"
Output:
OK
(error) WRONGTYPE Operation against a key holding the wrong kind of value
The fix is almost always a modeling one: don’t reuse the same key for two different shapes of data. If you need both a name field and a list under the same entity, namespace them separately, e.g. user:1001:name (string) and user:1001:recent_logins (list).
5. A GET-then-SET Race Instead of an Atomic Command
A tempting but broken way to implement a counter is to GET the current value, add one in your application, then SET it back. Between the GET and the SET, another client can do the same thing, and one increment gets silently lost. Use INCR (or INCRBY, DECR) instead — it reads and writes in one atomic server-side step.
SET counter:visits 10
INCR counter:visits
GET counter:visits
Output:
OK
(integer) 11
"11"
No matter how many clients call INCR counter:visits concurrently, every call applies atomically and none overwrite each other — the single-threaded execution model from the Overview section paying off directly.
Best Practices
- Namespace keys consistently, e.g.
entity:id:field(user:1001:email), so a large keyspace stays browsable and predictable. - Attach a TTL at write time with
SET ... EX secondsfor anything that’s genuinely a cache, instead of remembering to callEXPIREseparately afterward. - Use
KEEPTTLwhen updating a value that should keep its existing expiration. - Reach for type-specific atomic commands (
INCR,HINCRBY,SADD, etc.) instead of read-modify-write cycles in application code. - Never use
KEYSagainst a production dataset of meaningful size — useSCANand its type-specific cousins (HSCAN,SSCAN,ZSCAN) instead. - Pick the data type that matches your access pattern before writing code — a sorted set for a leaderboard, a hash for a record, a list for a queue — rather than forcing everything into strings with JSON blobs.
- Decide your persistence strategy (RDB, AOF, or both) deliberately based on how much data loss is acceptable on a crash.
Practice Exercises
- Store a product’s price under
product:2001:pricewith a 30-second TTL, confirm it withTTL, then update the price usingKEEPTTLand confirm the TTL is unchanged. - Create a hash
product:2001with fieldsname,price, andstock, then predict what happens if you runGET product:2001against it — then check whether you were right. - Set a key with no TTL, confirm with
TTLthat it returns-1, then useEXPIREto give it a TTL and confirm the change. Then tryEXPIREon a key name you know doesn’t exist and compare the return value.
Summary
- Redis is an in-memory data structure store — keys map to strings, hashes, lists, sets, sorted sets, or streams, not just plain values.
- Commands execute one at a time on a single main thread, which makes every individual command atomic without extra application-level locking.
- RDB snapshots and the AOF write log are the two persistence mechanisms, trading restore speed against durability; many setups use both.
- Keys expire through a mix of lazy checks on access and an active background cycle —
TTLreports remaining seconds,-1for no TTL,-2for a key that doesn’t exist. - A plain
SETon an existing key clears its TTL unless you addKEEPTTL. - Use
SCAN, notKEYS, against any dataset of real size. - Every key has exactly one type; mismatched commands fail with a
WRONGTYPEerror rather than silently doing the wrong thing.
