SwadhyAI Notes

All System Design notes

System Design Scalability

Redis

You open a ticket app during a sale. Ten servers are all asking: "Is seat A12 still free?" If each server only talks to the slow database, the page feels broken. You need one shared place they can all check in a few milliseconds.

That shared place is often : a fast in-memory store (data kept in RAM rather than on disk) that many servers share for caches, counters, locks, and rate limits. The catch, covered next, is that speed alone does not make ten servers agree. How you ask Redis matters just as much.

Learn Redis in 4 steps

1

What it is

Shared memory that many servers read in microseconds, with built-in data shapes.

2

Why teams use it

Speed, plus one shared answer so every server sees the same value.

3

How to stay correct

Keep check and change together with an atomic command or a short Lua script.

4

Where it fits

Caches, rate limits, locks, leaderboards, and small job queues that need speed.

Try it: fill Redis and watch eviction policy

Add keys with and without TTL, then keep writing until you hit maxmemory. LRU evicts the coldest key; volatile-lru ignores keys without TTL; noeviction refuses new writes instead of deleting data.

Memory

  • No keys stored yet.
Used memory
0 B
Evictions
0
Rejected writes
0
Policy
allkeys-lru

0 / 512 B · 0 keys · 0 evictions · 0 rejected writes · 0 hits / 0 misses. Redis is empty. Add keys until memory fills, then watch eviction policy decide what disappears.

Protect hot keys. A burst of new keys can evict a cold but critical session unless you size memory, set sensible TTLs, or use separate instances for cache versus durable structures.

A story that shows the bug

Here is the sale from the top of the page, slowed down. Seat A12 has 1 ticket left. Server A reads "1". Server B reads "1". Both sell. Now two customers have the same seat.

The mistake is not Redis itself. The mistake is making the decision in the app: read, think, write across two network calls. Between those calls another server sneaks in, and neither server can tell that it happened.

The fix is to move the decision next to the data: tell Redis "if stock is still above 0, subtract 1 and tell me OK, else say sold out" in one step. The two diagrams below show the broken and the safe version side by side.

Broken: two app steps

Each server reads, then writes. Another server sneaks in between.

Server Astock = 1Server B
1

A reads stock → 1

2

B also reads stock → 1

Both think they can sell
3

A writes stock → 0 (sells)

4

B writes stock → 0 (also sells)

Bug: two sales, one seat

Safe: one Redis step

Redis checks and updates in one atomic ask. The second server gets sold out.

Server ARedis decidesServer B
1

A asks Redis: “take 1 if available”

Check + change together → OK, now 0
2

B asks Redis: “take 1 if available”

Redis answers: sold out

Only one sale succeeds

Golden rule

Check and change must stay together

Atomic execution solves interleaving on the Redis instance; it does not by itself solve durability or coordination with another database. A stock decrement can be indivisible in Redis and still be lost during failover or disagree with an order transaction elsewhere. For a ticket sale, identify the authoritative reservation record and enforce the no-overselling rule there, or explicitly design a durable reservation protocol that connects Redis and the order store.

The ticket story is one instance of a general rule. Whenever correctness depends on the current value (stock, lock ownership, "already processed?", rate-limit count), do the check and the update inside Redis as one (a single command nobody can interrupt) or one (a small program Redis runs without interruption). App-side "GET then SET" will fail under real traffic.

Simple rule of thumb

Use Redis as shared fast memory, and keep the real long-term truth in your database unless you deliberately design otherwise. Prefer one built-in command; reach for Lua only when you need if/else logic next to the data. The rest of this page shows what those commands and scripts look like.

What actually happens on a read, and when Redis fits

Before the clever commands, it helps to see the plain path most Redis traffic takes. Your app opens a connection to Redis and sends a short command like GET. Redis answers from memory. If the key is missing (a cache miss), the app loads the value from the database, then SETs it into Redis with a (an expiry time) so stale copies clear themselves out later.

Redis handles one command at a time on its main loop (). That sounds limiting, but each command is tiny, so throughput is still huge for simple operations. It is also why the atomic commands later on this page are safe: nothing can interleave with a command that is already running.

This pattern pays off when many servers need the same hot data (sessions, popular profiles, feature flags), when you need a shared counter or limit such as "100 requests per minute per API key", or when you need a short lock or queue so only one worker processes a job right now.

The one condition is that the hot data fits in RAM. If the working set is huge and cold, keep Redis for the tip of the iceberg only; the Numbers to know section later on the page shows why memory is the budget you plan around.

Cache-aside path

Pick a shape for your data

The read path above used plain strings, but Redis is not only "string in, string out." Each type below is a shape the server understands natively, so choosing the right one saves a lot of app code. Two of them return later: a (unique items ordered by a score) powers the rate limiter in the Lua section, and a (an append-only event log) is the usual Redis queue when workers must acknowledge jobs.

String

One value per key, read and written as a whole.

Use for: Cache a JSON blob, a flag, or a counter (INCR).

Hash

A small object whose fields you can update one at a time.

Use for: user:42 → name, email, plan.

List

An ordered line of items you push to or pop from either end.

Use for: Simple queue or “last 20 notifications.”

Set

Unique items with no duplicates, however many times you add them.

Use for: Tags, online user ids, “who liked this.”

Sorted Set

Unique items with a score, kept in order by that score.

Use for: Leaderboards, “requests in last 60s.”

Stream

An append-only event log where workers acknowledge what they processed.

Use for: Workers that ack jobs and retry failures.

Also handy later: bitmaps (one bit per user, good for daily active flags), HyperLogLog (approximate unique counts in tiny memory), and geo commands (nearby drivers).

Atomic operations (start here)

With the shapes in hand, the first tool for the golden rule is also the simplest. Many problems do not need Lua at all, because one already does the whole check-and-change job in a single step.

INCR

Add 1 to a counter and return the new total.

Example: Page views, “how many seats sold,” simple fixed-window rate limits.

SET key value NX EX 30

Set the key only if it does not exist (NX), and expire in 30 seconds (EX).

Example: Try to take a lock. If NX fails, someone else already holds it.

GETDEL / GETEX

Read and delete, or read and refresh expiry, in one step.

Example: Avoid “read then delete” races between two servers.

ZADD + ZPOPMIN

Add scored items, or pop the lowest score, as one step.

Example: Priority queues and “run this job at time T.”

What about MULTI / EXEC?

MULTI queues commands and EXEC runs the queued sequence without other clients interleaving commands. Redis transactions do not provide SQL-style rollback of commands that already ran when a later command has a runtime error. WATCH offers optimistic concurrency: abort the transaction if watched keys changed before EXEC, then retry from a fresh read. Use a short script when the decision depends on values read during the atomic operation.

When one command is not enough, the next thing people reach for is a transaction. means "queue these commands, then run them back-to-back." It is useful when every step is known up front. It cannot branch ("if stock is 0, stop"), so for branching you need the Lua scripts in the next section.

Lua scripts: the checklist Redis runs alone

When a single command cannot express the decision and MULTI/EXEC cannot branch, a fills the gap. It is a short program you send to Redis; Redis runs every line before helping the next client, which is how "check then change" with if/else logic stays safe.

KEYS = the Redis key names you will touch. ARGV = numbers and strings (limits, timestamps, tokens). Keep scripts short, and make no network calls from inside Lua. Prefer to pass "now" from the app as ARGV so replicas (copies of the data on other machines) replay the same decisions.

Day to day: load the script once, then call it with (run it by its fingerprint). If Redis restarts and returns NOSCRIPT, load again and retry.

Example 1: rate limit (sliding window)

A sliding window counts requests in the last 60 seconds from right now, rather than in fixed clock minutes. Using a Sorted Set with score = timestamp, the script does four things:

  1. Forget every request older than 60 seconds, so old traffic stops counting against you.
  2. Count what remains in the window; that is the current usage for this key.
  3. If the count is already at the maximum, reject the request and report the count.
  4. Otherwise record this request with its timestamp and allow it through.

Those four steps must not be split across two app servers, or both could see room for one more request. The script below keeps them together.

lua
-- Plain English:
-- 1) Drop requests older than the window
-- 2) If count is already at max, reject
-- 3) Otherwise record this request and allow
local key = KEYS[1]
local window = tonumber(ARGV[1])
local max = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local member = ARGV[4]
local min_score = now - window

redis.call('ZREMRANGEBYSCORE', key, 0, min_score)
local count = redis.call('ZCARD', key)
if count >= max then
  return {0, count}   -- {allowed?, current_count}
end
redis.call('ZADD', key, now, member)
redis.call('PEXPIRE', key, window)
return {1, count + 1}

Example 2: release a lock safely

A unique lock token prevents an old owner from deleting a newer owner’s lock, but an expired lease does not stop the old owner from continuing external work. Imagine a worker pauses for 40 seconds while its 30-second lease expires; another worker acquires the lock and starts the same job. A protected storage system may need monotonically increasing fencing tokens to reject stale owners, or the side effect must be idempotent. A Redis lock alone cannot make an external action exactly once.

Taking a lock is often SET lock my-token NX EX 30 (the NX card above). Releasing it is trickier: you must delete the key only if it still holds your token. Otherwise you might delete the next owner's lock after your own expired.

lua
-- Only delete the lock if I still own it
if redis.call('GET', KEYS[1]) == ARGV[1] then
  return redis.call('DEL', KEYS[1])
else
  return 0   -- someone else owns it now
end

For money or inventory, also use a (a rising number the database checks on every write), so a paused old holder cannot write late.

If you need X, use Redis like this

Everything so far comes together in this lookup table. Two terms it uses: a fixed window resets the count every clock minute (INCR + EXPIRE), while a sliding window is the Lua example above; an idempotency key is a unique id per operation that stops a retried request from running twice.

You need…Simple approach
Faster profile readsGET/SET string (or Hash) with TTL. Delete key after a DB write.
Login sessionsStore session blob under session:<id> with TTL. Refresh TTL on activity.
Stop API abuseINCR + EXPIRE for a fixed window, or Sorted Set + Lua for a sliding window.
Only one worker at a timeSET NX EX to lock. Lua compare-and-delete to unlock.
Game leaderboardSorted Set: score = points. ZREVRANGE for top N.
Avoid double charge on retrySET NX idempotency:<key> with TTL. First writer wins.
Notify many servers livePUBLISH/SUBSCRIBE for fire-and-forget events.
Reliable small job queueStreams with consumer groups (XADD / XREADGROUP / XACK).
Run a job laterSorted Set where score = unix time to run.
Nearby driversGEOADD + GEOSEARCH, with short TTLs on stale GPS points.
Approx unique visitorsHyperLogLog (PFADD / PFCOUNT) when exact storage is too big.

Sizing, durability, and what still goes wrong

The table tells you which commands to use. This card covers the operational side that decides whether Redis stays healthy once real traffic arrives.

Numbers to know

2 million active sessions × about 1.2 KB each × ~1.3 for Redis overhead ≈ 3.1 GB of RAM for that working set. Add room for a replica and peak traffic. Redis is fast because it is memory-first, so memory planning is part of the design rather than an afterthought.

Will data survive a restart?

RDB snapshots capture periodic states, so recovery may miss writes since the last snapshot. AOF records writes, with the fsync policy controlling how much recent work can remain only in memory or operating-system buffers. Replication adds another copy but asynchronous replication can lag. Choose and test these settings according to the cost of losing a key: a cached profile can be rebuilt, while an authoritative reservation cannot simply be guessed. See Redis persistence documentation for the precise contract.

Because everything lives in RAM, you have to decide up front what happens after a crash. Three settings cover most cases.

Memory only

Fastest. Fine for pure cache. After restart, keys are gone until rebuilt.

Periodic snapshots. Easy to restore. You can lose the last few minutes.

Logs each write. Safer for recent changes. Slightly more disk work.

Need more than one machine? Use to split keys across shards (each machine owns a slice of the keys), with replicas for failover. Remember: async replicas can still lose the last unreplicated write during a crash.

Things that still go wrong

Long commands and scripts occupy execution time that other clients need. A large key, an unbounded scan inside a script, or an oversized response can therefore create latency spikes even when simple GET benchmarks look excellent. Bound collection sizes, prefer incremental iteration for large datasets, and measure slow commands, memory fragmentation, evictions, and client output buffers. Pipeline independent commands to reduce round trips without confusing pipelining with atomic execution.

Even a well-sized, durable setup has sharp edges. A can melt one shard. A huge KEYS scan can freeze Redis for everyone. A Sorted Set that never trims can eat all RAM. Clients without timeouts can hang during failover.

Treat Redis as shared infrastructure: watch memory, latency, evictions (keys Redis drops when memory is full), and replication lag. Keep a plan to rebuild important data from the database if Redis is empty after an incident.

Choosing and explaining Redis

A complete explanation connects the mechanism to a requirement. Describe what improves when you introduce it, which new failure cases become possible, and how you would know that the trade-off is worthwhile.

Explain the core idea

Shared fast memory with structures (string, hash, sorted set, stream).

Keep check-and-change together with one atomic command or a short Lua script.

Say whether data may vanish on restart (cache) or needs AOF/replicas/rebuild.

When it belongs

Many app servers need the same hot data or counters.

You need rate limits, locks, leaderboards, or short queues.

Database alone cannot meet the latency or QPS for that hot path.

How it fails

App-side GET then SET races under concurrency.

Hot keys, slow commands, and unbounded memory growth.

Failover or restart loses unreplicated writes or loaded scripts.

Trade-offs to weigh

Speed and convenience versus RAM cost and weaker durability than a primary DB.

Lua correctness versus operational care (Cluster slots, NOSCRIPT, short scripts).

Redis Streams convenience versus a dedicated broker at huge retention/fan-out.

Common misunderstandings

Calling Redis “just a cache” while storing the only copy of critical state.

Releasing locks with plain DEL (can delete another owner's lock).

Using KEYS * or giant Lua loops on the hot path.

Wrap-up

You have seen the race, the golden rule, the commands and scripts that enforce it, and the operational limits. Use these exercises to check the ideas stuck, then follow the official docs for the details.

Try these yourself

  1. Explain Redis to a friend. In three sentences: what it stores, why it is fast, and why the read-think-write pattern from the story can double-sell a ticket.
  2. Design a 100 requests / minute limiter. Draw the key name, choose a fixed window (INCR) or the sliding window from Example 1, and say what the API returns on reject.
  3. Size sessions. 1M online sessions, 800 bytes each, 24h TTL. Roughly how much RAM with 30% overhead? What changes if you add a replica?

Primary references

Official docs for data types, Lua scripting, and clustering.

Key takeaway

Redis is the shared whiteboard for your servers: fast memory, useful shapes, and safe updates when you follow the golden rule above. Start with one atomic command, use a short Lua checklist when the decision needs if/else, and plan memory and durability the same way you plan the feature.

Practice and further reading

Use these exercises to connect the lesson to a design decision. State your assumptions, trace what happens to one request, and explain the rule that must remain true even during a failure. That rule is the design's correctness invariant.

Focus for this lesson: Treat Redis as shared fast memory: pick a structure, keep check-and-change atomic (command or short Lua), and plan TTL/memory/durability.

Explain the decision

Why is GET then SET unsafe for selling the last ticket?

Read the reasoning

Two servers can both read stock=1 before either writes. Move check-and-update into one Redis command or a short Lua script.

Apply it to a production scenario

Two pods both sell seat A12. What do you change?

Read one approach

Replace app-side read/decide/write with an atomic DECR/guard or Lua that returns OK or sold-out in one step.

Work through the numbers

2M sessions × 1.2 KB × 1.3 overhead: roughly how much RAM?

Read the worked calculation

About 3.1 GB for the working set before replica and peak headroom.

Reason through a failure

What must lock release preserve?

Read the recovery reasoning

Delete or renew only when the token still matches; for critical writes prefer a fencing token checked by the resource.