Cloud Explainers

How Redis Caching Works

Caching & Redis10 min videoPublished Nov 7, 2026

Redis makes a slow page fast by keeping a copy of the answer in memory. We built it for real in a small tea shop: cache-aside with PostgreSQL behind it, expiry, a stale price, a full cache, and fifty visitors arriving the second a key expires.

How Redis Caching Works, the full picture: diagram of Visitors, Shop app, Redis, PostgreSQL, Data
The whole picture, from the video's recap.

The same slow answer

Your page asks the database the same question on every single visit, and waits for the same answer every time. Here's Ana's tea shop: the best sellers page takes 289 milliseconds. Run it again: the same five teas, the same wait. Nothing changed, but the database did all the work again.

laptop: Ana's terminal
python3 shop.py bestsellers --no-cachefrom PostgreSQL (no cache): 289.2 ms
1. Sencha           1,789,066 sold
2. Assam              739,497 sold
3. Earl Grey          567,389 sold
4. Jasmine Pearls     478,273 sold
5. Darjeeling         421,785 sold
laptop: Ana's terminal
python3 shop.py bestsellers --no-cachefrom PostgreSQL (no cache): 289.0 ms
1. Sencha           1,789,066 sold
2. Assam              739,497 sold
3. Earl Grey          567,389 sold
4. Jasmine Pearls     478,273 sold
5. Darjeeling         421,785 sold

The usual answer is: put Redis in front of it. And the usual worry comes right after. On Hacker News, back in 2011, people asked exactly that: if Redis keeps a copy, how do you keep that copy in step with the database?

A real question, Hacker News, 2011: diagram of Hacker News, 2011, Redis keeps a copy, The database changes

So we'll build it, piece by piece: the visitors, Ana's shop app, PostgreSQL, and Redis beside it. We'll take that page from 289 milliseconds to under one, make the copy go wrong on purpose, and fill the cache until it's full. And one question to keep in mind: when does a cache make the database's day worse instead of better?

What Redis is

Why is the page slow? To find the best sellers, PostgreSQL reads all 4,000,000 orders, adds up each tea, and sorts them. That's real work, every visit, for an answer that barely changes.

Rows read for one page view: figure of 4,000,000 orders added up, source: measured in the demo: 20 teas, 4,000,000 orders

Redis is a separate server program that keeps data in memory. You give it a key, a name, and a value, any text, and it gives the value back when you ask for that key. Memory is far faster to read than a disk, and Redis doesn't add up anything: it just hands back what it was given.

What Redis is: diagram of A server program, Keys and values, All in memory

Here it is, from Ana's laptop. PING: it answers PONG. redis-cli is Redis's own command line tool, and dash h cache says which server to talk to. SET greeting hello: OK, stored. GET greeting gives hello back. And a key that was never set gives nil: nothing there.

laptop: Ana's terminal
redis-cli -h cache PINGPONG
laptop: Ana's terminal
redis-cli -h cache SET greeting helloOK
laptop: Ana's terminal
redis-cli -h cache GET greeting"hello"
laptop: Ana's terminal
redis-cli -h cache GET nothing-here(nil)

Ask the cache first

Now the shop uses it. The pattern is called cache-aside. On each visit, the app asks Redis first. If the key isn't there, that's a cache miss: the app asks PostgreSQL, gets the five teas, and stores them in Redis for 300 seconds. The next visit is a cache hit: Redis has the answer, and PostgreSQL isn't asked at all.

Cache-aside: ask Redis first: diagram of Shop app, Redis, PostgreSQL, GET shop:bestsellers, nil: a miss, best sellers query, five teas, SET... for 300 s, next visit: GET, five teas: a hit, On a hit, PostgreSQL isn't asked

In Ana's code, that's four steps. Get the key from Redis. If something came back, use it. If not, run the slow query. Then SET the answer with ex equals 300: expire after 300 seconds. What Redis stores is the five teas written out as JSON, plain text.

Cache-aside in shop.py: cards of 1. Ask Redis, 2. A hit: use it, 3. A miss: ask PostgreSQL, 4. Keep it 300 s

First visit: a cache miss, 299 milliseconds, the same work as before, and the answer goes into Redis. Now the second visit. Guess first: how long will it take? 0.6 milliseconds, from Redis, the same five teas, and the database never even heard about this visit.

laptop: Ana's terminal
python3 shop.py bestsellersfrom PostgreSQL (cache miss): 298.7 ms
1. Sencha           1,789,066 sold
2. Assam              739,497 sold
3. Earl Grey          567,389 sold
4. Jasmine Pearls     478,273 sold
5. Darjeeling         421,785 sold
laptop: Ana's terminal
python3 shop.py bestsellersfrom Redis (cache hit): 0.6 ms
1. Sencha           1,789,066 sold
2. Assam              739,497 sold
3. Earl Grey          567,389 sold
4. Jasmine Pearls     478,273 sold
5. Darjeeling         421,785 sold

Side by side: 299 milliseconds from PostgreSQL, 0.6 from Redis. About 500 times faster, and the database does nothing at all on a hit.

Best sellers page, measured: chart of PostgreSQL (miss): 298.7 ms, Redis (hit): 0.6 ms, About 500x faster on a hit

That name is worth a moment. shop, colon, bestsellers. The colon means nothing to Redis: it's a habit, so every key the shop makes starts with shop, and you can tell at a glance which app a key belongs to.

A key name: diagram of shop:bestsellers, shop = which app, : = separator, bestsellers = which answer, To Redis, the whole name is one string

Answers that expire

Why expire it at all? Because orders keep coming in, and the best sellers should catch up eventually. Redis can tell you how long a key has left: the TTL command, time to live. Right after that first visit, it said 297 seconds, counting down.

Time to live of shop:bestsellers: figure of 297s seconds left, source: redis-cli -h cache TTL shop:bestsellers: (integer) 297

So the copy in Redis has a clock on it. Every visit inside those five minutes gets a cache hit. When the clock runs out, Redis deletes the key by itself, and the next visit is a miss that asks PostgreSQL for a fresh answer.

We waited the real five minutes. TTL now says minus 2: the key doesn't exist any more. So the next visit is a miss again, 294 milliseconds, and Redis has a new copy for the next five minutes.

laptop: Ana's terminal
redis-cli -h cache TTL shop:bestsellers(integer) -2
laptop: Ana's terminal
python3 shop.py bestsellersfrom PostgreSQL (cache miss): 293.6 ms
1. Sencha           1,789,066 sold
2. Assam              739,497 sold
3. Earl Grey          567,389 sold
4. Jasmine Pearls     478,273 sold
5. Darjeeling         421,785 sold

When the copy is wrong

Now the worry from Hacker News. The shop also caches each tea's page. Earl Grey: $10.50, a miss, so that page is in Redis now. Ana raises the price to $11.50 in the database. Guess first: what does the next visitor see? The old price, $10.50, straight from Redis, with 297 seconds still to go.

laptop: Ana's terminal
python3 shop.py price 3Earl Grey: $10.50   (from PostgreSQL (cache miss))
laptop: Ana's terminal
python3 shop.py set-price 3 11.50database: product 3 now $11.50
laptop: Ana's terminal
python3 shop.py price 3Earl Grey: $10.50   (from Redis (cache hit))
laptop: Ana's terminal
redis-cli -h cache TTL shop:product:3(integer) 297

Here's why. The update went to PostgreSQL only. Redis was never told, so it keeps serving its copy until the TTL runs out. That's stale data: a cached answer that's no longer true.

The price changed, the copy didn't: diagram of Ana, PostgreSQL, Redis, Visitor, price: $11.50, Redis isn't told, GET product 3, $10.50 (stale), Stale data: a cached answer that's no longer true

The fix: when you change the data, delete the cached copy too. Ana's admin page now does both: it updates PostgreSQL, then deletes the key. The next visit is a miss, and it reads the new price, $11.50.

laptop: Ana's terminal
python3 shop.py set-price 3 11.50 --clear-cachedatabase: product 3 now $11.50
redis: deleted 1 key
laptop: Ana's terminal
python3 shop.py price 3Earl Grey: $11.50   (from PostgreSQL (cache miss))

So there are two safety nets, and most shops use both. Delete the key whenever you write, and keep a TTL anyway, so that a write you forgot to handle can only be wrong for a few minutes. And pick the TTL from the question: how long could this answer be out of date before anyone minds?

Keeping the copy right: cards of Delete on write, Keep a TTL anyway, Pick the TTL, Never the only copy

When memory runs out

Memory is fast, but it's small and expensive, so you tell Redis how much it may use. We started this one with a limit of 20 megabytes: maxmemory. And a policy says what happens when it's full. Redis's default is noeviction: keep everything, and see what happens next.

How much memory this Redis may use: figure of 20 MB maxmemory, source: redis-cli CONFIG GET maxmemory: 20971520 bytes

Here are both settings. Now Ana caches a page for every product in a big catalogue: 40,000 pages, a kilobyte each. Page 17,376 fails: command not allowed when used memory is over maxmemory. With noeviction, a full Redis refuses new writes. It's at 19.87 megabytes, right at its limit.

laptop: Ana's terminal
redis-cli -h cache CONFIG GET maxmemory1) "maxmemory"
2) "20971520"
laptop: Ana's terminal
redis-cli -h cache CONFIG GET maxmemory-policy1) "maxmemory-policy"
2) "noeviction"
laptop: Ana's terminal
python3 shop.py fill 40000page 17,376 failed: command not allowed when used memory > 'maxmemory'.
laptop: Ana's terminal
redis-cli -h cache INFO memory | grep used_memory_humanused_memory_human:19.87M

For a cache, there's a better policy: allkeys-lru, least recently used. Same 40,000 pages: all stored. To make room, Redis evicted 22,619 keys. The first page is gone, the newest is there.

laptop: Ana's terminal
redis-cli -h cache CONFIG SET maxmemory-policy allkeys-lruOK
laptop: Ana's terminal
python3 shop.py fill 40000cached 40,000 pages
laptop: Ana's terminal
redis-cli -h cache INFO stats | grep evicted_keysevicted_keys:22619
laptop: Ana's terminal
redis-cli -h cache EXISTS shop:page:1(integer) 0
laptop: Ana's terminal
redis-cli -h cache EXISTS shop:page:40000(integer) 1

The rule behind it is least recently used: when memory is full, delete the key nobody has touched for the longest. A key that's read all the time counts as just used, so a busy page stays. The page written a moment ago stays too. Page 20,000, written a while ago, is next in line. And page 1, untouched the longest, goes first. Redis doesn't sort every key to find the oldest: it checks a small sample and evicts the oldest of those, which is close enough, and fast.

allkeys-lru: who gets evicted: diagram of A busy page, Page 40,000, Page 20,000, Page 1

So noeviction keeps everything and refuses new writes, which suits data you can't lose. allkeys-lru always accepts the write and forgets old keys, which suits a cache, because every key can be rebuilt from the database.

When Redis is full: comparison table of noeviction (default) vs allkeys-lru, New write, Old keys, Suits, Why it's safe

Fifty visitors at once

Now the question from the start: when does a cache make the database's day worse? The second a popular key expires. Fifty visitors open the best sellers page at the same moment, all of them miss, and 50 of them ask PostgreSQL the same slow question at once. The slowest waited 7,197 milliseconds.

laptop: Ana's terminal
python3 shop.py rush 5050 visitors at once
asked PostgreSQL: 50
served by Redis:  0
slowest visitor:  7,197 ms

This is called a cache stampede. While the cache was warm, PostgreSQL rested, so nobody noticed how heavy the query was. Then one expiry sends everyone through at once, and each query gets slower because the others are running too.

The fix is a lock, kept in Redis itself. Each visitor that misses tries SET with nx: only if the key doesn't exist yet. Exactly one visitor gets OK, and runs the query. Everyone else gets nil, waits a moment, and asks Redis again. The lock has its own ten-second TTL, so a crashed visitor can't hold it forever.

Only one visitor rebuilds it: diagram of Visitor 1, Visitor 2, Redis, PostgreSQL, SET lock NX, OK: you rebuild, best sellers query, nil: wait, SET the answer, GET: a hit, The lock expires after 10 s by itself

Same fifty visitors, with the lock. This time the database was asked just once. The other 49 got the answer from Redis, and the slowest waited 348 milliseconds instead of 7,197.

laptop: Ana's terminal
python3 shop.py rush 50 --lock50 visitors at once (with a lock)
asked PostgreSQL: 1
served by Redis:  49
slowest visitor:  348 ms

In numbers: 50 slow queries became one.

Queries PostgreSQL ran for 50 visitors: chart of No lock: 50, With a lock: 1, Slowest visitor: 7,197 ms vs 348 ms

Caching in real life

Big sites do exactly this. Stack Overflow describes it on its engineering blog: Redis for shared caching behind their web servers. Each server checks its own memory first, then Redis. In 2019 they measured about 1.6 billion Redis commands a day, with small values back in a fraction of a millisecond.

Stack Overflow, engineering blog, 2019: diagram of Web servers, Redis, The database, Caches

And remember what a cache is: a copy. FLUSHALL wipes every key in Redis. The next visit is slow once, 286 milliseconds, and still correct, because PostgreSQL still has everything.

laptop: Ana's terminal
redis-cli -h cache FLUSHALLOK
laptop: Ana's terminal
python3 shop.py bestsellersfrom PostgreSQL (cache miss): 285.8 ms
1. Sencha           1,789,066 sold
2. Assam              739,497 sold
3. Earl Grey          567,389 sold
4. Jasmine Pearls     478,273 sold
5. Darjeeling         421,785 sold

So when should you cache? When the same answer is read again and again, it's slow to make, and being a little out of date is fine. Not for the moment money or stock changes hands: there, read the database.

Cache it, or not?: comparison table of Cache it vs Read the database, Read, Cost, Out of date, Example

Ana's whole lab, the shop code plus every command, is linked in the description, so you can run the cache yourself.

Run it yourself: cards of shop.py, schema.sql, redis-cli, Dockerfile

Recap

So: ask Redis first, fill it on a miss with a TTL, delete on write, let it evict when it's full, and lock the rebuild of a busy key.

If this made caching click, subscribing gets you the next one, where we take another piece of the stack apart.

Every number here came from Ana's real shop. Thanks for watching Cloud Explainers.

Sources

Demos: containers on our own lab server standing in for Ana's shop and laptop; the tea shop is made up. Watch the full video on YouTube.

More videos

Linux9 min

Linux Makefiles Will Finally Make Sense After This

Auth (OAuth, JWT)9 min

SAML vs OAuth vs OpenID Connect: What's the Difference?

Python7 min

How to Fix Python cron Jobs That Never Run

Linux22 min

Linux Bash Scripting: Full Tutorial for Beginners

Networking10 min

How PXE Network Boot Works

Active Directory9 min

How Active Directory Works

Linux10 min

How to Fix a Linux systemd Service That Won't Start

Linux9 min

Linux Logs: syslog vs journald

Auth (OAuth, JWT)9 min

JWT vs Session Cookies: Which One Should You Use?

Docker10 min

Docker vs Docker Compose: Which One Should You Use?

Docker9 min

How to Fix Docker Compose Containers That Can't Talk to Each Other

Django (Python web framework)22 min

Django Full Tutorial for Beginners

Linux9 min

How Linux systemd Works

Networking8 min

How to Fix 'Windows Cannot Access' Network Share Errors (SMB)

APIs10 min

REST vs gRPC: Which One Should You Use?

Certificates & PKI (ADCS)9 min

How to Fix SSL Certificate Chain Errors (Missing Intermediate)

Python9 min

Every Python Concept for Beginners in 10 Minutes

Databases & SQL9 min

Every SQL Join Type in 10 Minutes

OpenRouter (one API for many AI models)9 min

Why OpenRouter Free Models Hit Their Limit So Fast

Gemini API & Google AI Studio9 min

Why Your Gemini API Free Tier Runs Out So Fast

OpenRouter (one API for many AI models)10 min

OpenRouter Will Finally Make Sense After This

Gemini API & Google AI Studio10 min

Gemini API vs Vertex AI: Which One Should You Use?

Gemini API & Google AI Studio8 min

How to Fix a Leaked Gemini API Key

Go (Golang)9 min

Why Golang Uses Goroutines Instead of Threads

Gemini API & Google AI Studio9 min

How to Fix Gemini API Error 429: Resource Exhausted

Claude & the Claude Developer Platform10 min

Claude Token Pricing Will Finally Make Sense After This

Python11 min

Why Python Web Scraping Returns Nothing (and the Fix)

Python10 min

Python uv vs pip: Which One Should You Use?

Python22 min

Python Automation: Full Tutorial for Beginners

Auth (OAuth, JWT)9 min

How OAuth Login Works

Gemini API & Google AI Studio10 min

Google AI Studio Will Finally Make Sense After This

Docker10 min

What Happens When You Run a Docker Container

Claude & the Claude Developer Platform8 min

How to Fix Claude Code Billing Your API Key Instead of Your Plan

Auth (OAuth, JWT)9 min

Why Passkeys Can't Be Phished Like Passwords

PowerShell10 min

PowerShell vs Bash: Which One Should You Learn?

Linux10 min

The Linux Roadmap for Beginners

Django (Python web framework)9 min

Django vs FastAPI: Which One Should You Use?

Databases & SQL10 min

Why SQLite Is the Most Used Database in the World

Databases & SQL11 min

How a NoSQL Database Like MongoDB Works

Python11 min

How to Fix Python pip error: externally-managed-environment

Databases & SQL9 min

How to Fix MySQL Too Many Connections Database Errors

Go (Golang)9 min

Golang Concurrency Will Finally Make Sense After This

AWS10 min

How Amazon S3 Works

Kubernetes10 min

How Kubernetes Works

Git10 min

How Git Works

Docker10 min

How to Fix Docker Port Already Allocated Errors

PostgreSQL10 min

How PostgreSQL Works

Git8 min

How to Fix Git Merge Conflicts

Linux9 min

How to Fix No Space Left on Device in Linux

Networking10 min

How the Internet Works

Databases & SQL9 min

SQL vs NoSQL: When Each One Wins

Auth (OAuth, JWT)8 min

How to Fix JWT Token Expired Errors (401 Unauthorized)

Claude & the Claude Developer Platform9 min

Claude API vs Claude Pro: Why Your Subscription Doesn't Include It

AI agent skills9 min

Why AI Agent Skills Beat Giant Prompts

Linux10 min

Linux Cron Jobs Will Finally Make Sense After This

Cybersecurity10 min

Security Logs Will Finally Make Sense After This

Cybersecurity8 min

How to Fix a Security Leak: An API Key Pushed to GitHub

DNS9 min

DNS Will Finally Make Sense After This

Cybersecurity9 min

Never Ignore a Security Update

Auth (OAuth, JWT)9 min

Two-Factor Authentication Will Finally Make Sense

Networking8 min

Never Expose a Database to the Internet

Cybersecurity10 min

SSH Keys vs Passwords: Which Is Safer for Security?

Linux10 min

The 5 Levels of Linux Boot: From Power On to Login

Networking12 min

The 5 Levels of Network Firewalls

Cybersecurity10 min

Password Security: Why Hackers Crack Weak Passwords

Linux9 min

Why Your Linux Server Runs Out of Memory

Networking10 min

HTTP vs HTTPS: What Actually Changes on Your Network

Linux12 min

Every Linux File Permission You Need to Know

Linux10 min

Linux Processes vs Threads: When Each One Wins

Networking11 min

The OSI Model Will Finally Make Sense After This

Git11 min

The 7 Levels of Git

Networking10 min

Every Network Troubleshooting Command You Need

Cybersecurity10 min

Hashing vs Encryption in Cybersecurity: When Each One Wins

Networking11 min

Why the Internet Ran Out of IP Addresses

Linux11 min

Linux Package Managers: apt vs Snap vs Flatpak

Python9 min

Why Python Is Slow (and When It Doesn't Matter)

Networking11 min

TCP vs UDP: When Each One Wins

Linux8 min

Stop Running Linux as Root (Do This Instead)

Linux11 min

Linux Users and Groups Will Finally Make Sense

Cybersecurity9 min

Why Phishing Beats Good Security (and How to Spot It)

Docker12 min

The 7 Levels of Docker

AI agents & MCP12 min

AI Agents Will Finally Make Sense After This

Git8 min

Stop Using git push --force (Use This Instead)

AI agent skills10 min

Agent Skills vs MCP: When Each One Wins

Networking12 min

Networking Will Finally Make Sense After This

Linux12 min

Every Linux Command You Actually Need

Cybersecurity12 min

The 7 Levels of Cybersecurity

System design12 min

Every System Design Concept You Need, in One Diagram

PostgreSQL10 min

Why PostgreSQL Ignores Your Index