Redis Fundamentals for Backend Developers
I walk through Redis data structures, TTL, the four main use cases, and RDB vs AOF persistence to give you the mental model before caching strategies.

Most tutorials introduce Redis as “the thing you put in front of your database.” Set a value, get a value, set an expiry. That model is technically correct but too small to understand why Redis is designed the way it is — or when to reach for it instead of adding another index to your database.
Redis calls itself “a data structure server.” That phrase is precise. It is not a cache with a fast lookup bolted on. It is an in-memory server that understands five different data types natively, lets you run atomic operations on them directly, and gives you per-key expiry as a first-class feature. Knowing those data types — what they store and what you can do with them — changes how you think about backend problems.
Quick answer: Redis stores everything in RAM and exposes five data types (String, List, Hash, Set, Sorted Set), each with its own atomic commands. Every key can carry a TTL. Two persistence modes — RDB snapshots and AOF logs — trade recovery speed for durability. The post ends with the four use cases Redis fits well and the cases where it’s the wrong tool.
On this page
- What Redis actually is
- The five data structures
- TTL and key expiry
- Four use cases that fit Redis well
- Persistence: RDB vs AOF
- When Redis is the wrong choice
What Redis actually is
Redis keeps all data in RAM. Reads and writes are fast because the data is already in memory — there is no disk I/O on the critical path. That is the “in-memory” part most developers already know.
The “data structure server” part is what distinguishes it from a plain key-value cache. A plain cache stores opaque blobs: you put a string in, you get a string out. Redis understands what type of value a key holds and exposes operations native to that type. You do not fetch a list, mutate it in application code, and write it back. You ask Redis to push an element onto the list, and Redis does that atomically in a single round trip, with no race condition between simultaneous requests.
This matters in practice. If two Node.js workers both try to increment a page-view counter, a naive read-modify-write approach produces incorrect results under concurrency. With Redis INCR, both workers send one command and Redis serializes them. The counter is always correct, without any application-level locking.
One thing worth knowing: Redis is single-threaded for command execution. That is not a disadvantage for most workloads — each command is fast and there is no thread contention, so throughput is high. It does mean a slow command (like scanning millions of keys) blocks every other client. Keep that in mind when you’re debugging unexpected latency in a shared Redis instance.
The five data structures
String is the base type. It holds any binary sequence — a number, serialized JSON, a session token. SET and GET are the obvious commands. Less obvious: INCR, INCRBY, and DECR turn a string into an atomic counter without any locking.
SET page_views 0
INCR page_views # returns 1
INCRBY page_views 5 # returns 6
SET cache:product:88 "{...json...}" EX 300
List is a doubly-linked list of strings. RPUSH adds to the tail, LPUSH to the head. LPOP/RPOP remove from either end. LRANGE reads a slice without removing elements. The same structure works as a queue (push to tail, pop from head) or a stack (push and pop from the same end).
RPUSH jobs "send-welcome-email"
RPUSH jobs "resize-avatar"
LPOP jobs # returns "send-welcome-email"
LRANGE jobs 0 -1 # all remaining items
Hash is a map of field-value pairs stored under one key — like a Redis object. HSET sets fields, HGET reads one, HGETALL reads all of them. The useful property: you can read or write individual fields without fetching or replacing the entire object. HINCRBY increments a numeric field atomically.
HSET user:42 name "Aditya" email "aditya@example.com" plan "pro"
HGET user:42 plan # returns "pro"
HINCRBY user:42 credits 10 # atomically increments credits
Set is an unordered collection of unique strings. SADD adds members, SISMEMBER checks membership in O(1) regardless of set size. The real utility is in set operations: SINTER gives you the intersection of two or more sets, SUNION the union, SDIFF the difference — useful for “which users have both of these permissions” or “which tags appear in all three posts.”
SADD tags:post:1 "nodejs" "backend" "redis"
SADD tags:post:2 "redis" "caching" "backend"
SINTER tags:post:1 tags:post:2 # returns {"redis", "backend"}
Sorted Set is a set where every member carries a floating-point score. Members are always ordered by score. ZADD adds a member, ZRANGE returns members by ascending rank, ZREVRANGE by descending rank, and ZRANGEBYSCORE returns members within a score range. Adds and lookups are O(log N) because the underlying implementation uses a skip list.
ZADD leaderboard 1500 "player:7"
ZADD leaderboard 2200 "player:3"
ZADD leaderboard 1800 "player:9"
ZREVRANGE leaderboard 0 2 WITHSCORES # top 3 players with scores
ZRANK leaderboard "player:9" # rank of a specific player
Sorted sets are the right structure for leaderboards, rate-limiting windows, and anything requiring ordered data with fast range queries.
TTL and key expiry
Any Redis key can carry a time-to-live. When the TTL reaches zero, Redis deletes the key automatically — no application-level cleanup needed.
SET session:xyz "{...}" EX 1800 # expires in 30 minutes
EXPIRE cache:product:88 300 # 5-minute TTL on existing key
PEXPIRE lock:order:99 500 # 500 milliseconds
TTL session:xyz # seconds remaining (-2 if gone, -1 if no TTL)
Redis combines two expiry strategies. Lazy expiry: a key is deleted the moment you access it and it has expired. Active expiry: a background process periodically samples keys with TTLs and removes the ones that have expired. Together, keys disappear close to their deadline without you doing anything extra, and memory does not silently fill up with stale data.
TTL is what makes Redis feel natural for sessions, temporary tokens, cached query results, and anything with an inherent shelf life. You write the value, declare how long it should live, and Redis takes care of the rest.
Four use cases that fit Redis well
Caching expensive results. Store the output of a slow database query or external API call with a short TTL. The next request hits Redis instead of the origin. When the TTL expires, the next caller regenerates the cache. The pattern is straightforward; the trade-offs around invalidation and staleness are the nuanced part, which the next post in this series covers in detail.
Session storage. When a user signs in through your REST API, write their session data to a Hash under a session key with a TTL. Refresh the TTL on each authenticated request. This works correctly across multiple server instances because every instance reads from the same Redis — sessions are not pinned to a specific machine. When the key expires, the session ends automatically.
Rate limiting. Keep a counter per user per time window: INCR rate:user:42:1725580800. Set a TTL equal to the window length on the first write. Check the counter value before processing each request — if it exceeds the limit, reject. The key disappears when the window closes and the counter resets for free. A sorted set of timestamps gives you a sliding window if fixed windows aren’t precise enough.
Pub/Sub messaging. Publishers send messages to named channels with PUBLISH. Any subscriber on that channel receives the message immediately. This fits lightweight real-time broadcasting — cache invalidation signals across servers, live notifications when an order status changes. One hard limit: if no subscriber is connected when the message arrives, it is lost. Redis Pub/Sub has no persistence and no delivery guarantees. For guaranteed-once delivery or replay, you need Redis Streams or a dedicated message broker.

Redis connects one server to five different data models. Each type has its own atomic operations, and any key can carry a TTL.
Persistence: RDB vs AOF
Because Redis stores everything in RAM, a crash without persistence means data loss. Redis offers two persistence modes — you can run either one, or both together.
RDB (Redis Database) writes point-in-time snapshots of the full dataset to a binary file (dump.rdb). You configure how often snapshots happen — for example, every 60 seconds if at least 1,000 keys have changed. The snapshot process forks a child that writes the file while the parent keeps serving requests without interruption. Restart recovery is fast: Redis loads one compact file. The trade-off: any writes since the last snapshot are lost if the server crashes between snapshots.
AOF (Append Only File) logs every write command in order. On restart, Redis replays the log to reconstruct state. With appendfsync everysec — the default — Redis syncs the log to disk once per second, so the maximum data loss is one second of writes. AOF files grow over time; Redis compacts them automatically by rewriting only the minimal set of commands needed to reproduce the current dataset.
The Redis documentation recommends running both modes if you want durability similar to a relational database. RDB alone is fine when losing a few minutes of data is acceptable. No persistence at all makes sense when Redis is purely a cache and the authoritative data lives in your database — a PostgreSQL instance, for example.
Check this before moving on
- RDB snapshots can lose writes since the last snapshot (potentially several minutes of data)
- AOF with
everyseccan lose at most one second of writes - RDB recovers faster on restart; AOF offers stronger durability
- Both modes can run simultaneously in the same instance
When Redis is the wrong choice
Redis’s strengths come with real constraints. Everything lives in memory, which means your working dataset must fit in RAM. Using Redis as a primary store for hundreds of gigabytes of relational data is expensive and the wrong fit.
Redis has no relational model. No joins, no ad-hoc queries across keys, no foreign key constraints. If you need “find all orders where total > $50 placed by users who signed up in January,” that query belongs in PostgreSQL, not Redis. Redis works alongside your database, not instead of it.
Pub/Sub has no delivery guarantees and no message history. If a subscriber disconnects and reconnects, it misses everything sent while it was gone. That is a hard architectural limit, not a configuration problem.
The practical rule is simple: Redis is the right choice when you need fast, temporary, or ordered data access with simple atomic operations. It is the wrong choice when you need complex queries, relational data models, large datasets that exceed your RAM budget, or durable messaging.
Once this model is clear — an in-memory data structure server with five types, built-in expiry, and configurable durability — the caching strategies in the next post follow naturally from it.