On this page:
Learning Objectives
1. Motivation:   Why Redis?
1.1 The problem:   the same expensive call, over and over
1.2 What Redis is
1.3 The smallest possible example
2. Installing and Connecting to Redis
2.1 Running the server with Docker
2.2 Connecting from Python
3. Redis Strings
3.1 Worked example:   a user-profile cache field
3.2 Worked example:   atomic counters
4. Expiration:   Building the AI Answer Cache
4.1 EX and TTL
4.2 Worked example:   caching the Terp  Tasks AI assistant
5. Redis Lists:   Queues
5.1 Producer and consumer
5.2 The problem with RPOP:   a worker that spins
6. Building a Background Job Queue
6.1 Why background work at all?
6.2 Job metadata:   a Redis Hash
6.3 BRPOP:   a worker that waits instead of spins
6.4 Putting it together:   submit → queue → process → query
7. Redis Sets
7.1 Worked example:   deduplication
7.2 Worked example:   set algebra for recommendations
8. Redis Hashes, Revisited
9. Redis Persistence
9.1 RDB:   periodic snapshots
9.2 AOF:   an append-only log of every write
10. Redis in Modern AI Systems
11. The Complete Project:   terptasks-redis
11.1 Repository structure
11.2 docker-compose.yml
11.3 app/  cache.py the AI answer cache
11.4 app/  queue.py the job queue
11.5 app/  stats.py counters
11.6 Testing without a real Redis server
11.7 Running the demo end to end
Practice Exercises
Basic
Intermediate
Advanced
Summary
Key takeaways
Terminology
Common mistakes
Connections
Instructor Notes
9.1

12 Redis: Building Fast, Stateful Applications๐Ÿ”—

    Learning Objectives

    1. Motivation: Why Redis?

      1.1 The problem: the same expensive call, over and over

      1.2 What Redis is

      1.3 The smallest possible example

    2. Installing and Connecting to Redis

      2.1 Running the server with Docker

      2.2 Connecting from Python

    3. Redis Strings

      3.1 Worked example: a user-profile cache field

      3.2 Worked example: atomic counters

    4. Expiration: Building the AI Answer Cache

      4.1 EX and TTL

      4.2 Worked example: caching the TerpTasks AI assistant

    5. Redis Lists: Queues

      5.1 Producer and consumer

      5.2 The problem with RPOP: a worker that spins

    6. Building a Background Job Queue

      6.1 Why background work at all?

      6.2 Job metadata: a Redis Hash

      6.3 BRPOP: a worker that waits instead of spins

      6.4 Putting it together: submit → queue → process → query

    7. Redis Sets

      7.1 Worked example: deduplication

      7.2 Worked example: set algebra for recommendations

    8. Redis Hashes, Revisited

    9. Redis Persistence

      9.1 RDB: periodic snapshots

      9.2 AOF: an append-only log of every write

    10. Redis in Modern AI Systems

    11. The Complete Project: terptasks-redis

      11.1 Repository structure

      11.2 docker-compose.yml

      11.3 app/cache.py the AI answer cache

      11.4 app/queue.py the job queue

      11.5 app/stats.py counters

      11.6 Testing without a real Redis server

      11.7 Running the demo end to end

    Practice Exercises

      Basic

      Intermediate

      Advanced

    Summary

      Key takeaways

      Terminology

      Common mistakes

      Connections

    Instructor Notes

This lecture builds two small pieces of TerpTasks the task-tracking API you already wrote in the REST/FastAPI lecture and gave a face to in the React lecture — using Redis: an AI answer cache (so an LLM-backed “ask about my tasks” feature doesn’t recompute the same answer twice) and a background job queue (so uploading a syllabus PDF to summarize doesn’t make the web server wait). Both pieces come together in one runnable project in 11. The Complete Project: terptasks-redis.

Learning Objectives๐Ÿ”—

By the end of this lecture, you should be able to:

  1. Explain what Redis is and when to reach for it contrast an in-memory key-value store against a disk-backed database like PostgreSQL, and identify which parts of an application’s state belong in each.

  2. Use Redis’s core data structures strings, lists, sets, and hashes — from both redis-cli and the Python client, and choose the right structure for a given problem.

  3. Build a cache with expiration use TTL to store the result of an expensive operation (an LLM call) for a bounded time, and explain why a cache without expiration is a memory leak with extra steps.

  4. Build a producer/consumer job queue use Redis Lists (LPUSH/BRPOP) so a slow background task never blocks a web request, and track per-job status with a Redis Hash.

  5. Explain Redis persistence trade-offs describe what RDB snapshots and AOF command logs each protect against, and how much data each can lose on a crash.

  6. Place Redis in a modern AI system architecture diagram where a cache, a task queue, a vector database, and an LLM each sit, and say which layer owns which kind of state.

  7. Recognize the most common Redis mistakes keys with no TTL, using Redis as a system of record, and blocking a web request on synchronous background work.

1. Motivation: Why Redis?๐Ÿ”—

1.1 The problem: the same expensive call, over and over๐Ÿ”—

Suppose TerpTasks grows an “Ask AI” box: a student types a question — “What’s due this week for CMSC389A?” and the server calls an LLM to answer it. That call is slow (a few seconds) and costs money per token. Twenty students in the same section ask the identical question within the hour. Without anything in between, every single one of those twenty requests re-runs the full LLM call:

Student 1  ──┐

Student 2  ──┤

   ...       ├──►  Application  ──►  LLM API  (slow + $$$, every time)

Student 20 ──┘

The fix is the oldest idea in computing: remember the answer you already computed. That’s a cache and Redis is the tool the industry reaches for to build one, because it answers GET in well under a millisecond:

Student 1  ──┐

Student 2  ──┤              ┌─────────┐   miss   ┌─────────┐

   ...       ├──► App ─────►│  Redis  │─────────►│ LLM API │

Student 20 ──┘                Cache  │◄─────────┘ (once)  

                             └─────────┘   store

The first student to ask pays the LLM’s latency and cost; Redis stores the answer; every subsequent identical question is a cache hit answered in microseconds, for free. This exact pattern — LLM cache in front of an LLM call — is the running example for the first half of this lecture, and we will keep building on it until §11.

1.2 What Redis is๐Ÿ”—

Redis (REmote DIctionary Server) is an in-memory, key-value data store: it holds all of its data in RAM and exposes a small set of rich data structures (not just strings) through a simple command protocol.

Plain English. Redis is a giant, extremely fast dictionary/hash-map that lives outside your application process, so multiple processes — your web server, a background worker, a second web server behind a load balancer — can all read and write the same shared state.

Intuition. A Python dict in your web server’s memory is fast but private to that one process, and it vanishes when the process restarts. Redis is that same dict, pulled out into its own long-lived process, reachable over the network, and shared by everyone. You trade a tiny bit of latency (a network round trip, still sub-millisecond on localhost) for sharing and persistence across restarts of your app.

Compare Redis to the databases you already know:

Database

Primary storage

Typical use

PostgreSQL

Disk

The permanent record — users, tasks, orders

MongoDB

Disk

Permanent, flexible-schema documents

Redis

RAM (optionally backed up to disk)

Speed: caches, queues, counters, session state

Common misconception: “Redis is a replacement for my database.” It is not, and shouldn’t be. Redis can persist to disk (§8), but its whole design center is speed, not durability guarantees or rich querying. The rule of thumb this lecture builds toward: if losing this data would be catastrophic and unrecoverable, it belongs in Postgres; if losing it means “recompute it” or “a user re-logs-in,” it belongs in Redis.

1.3 The smallest possible example๐Ÿ”—

Every Redis command follows the same shape: a verb, a key, and (sometimes) a value. Start the server (§2) and connect with its command-line client, redis-cli:

SET user:1001 "Alice"
GET user:1001

"Alice"

That’s the whole mental model in two lines: SET stores a value under a key, GET retrieves it. Everything else in this lecture is “what other verbs exist, and what other shapes of value can a key hold besides a string.”

2. Installing and Connecting to Redis๐Ÿ”—

2.1 Running the server with Docker๐Ÿ”—

The fastest way to get a real Redis server running locally is Docker — one container, no system-wide install:

docker run \
  --name redis \
  -p 6379:6379 \
  -d redis

6379 is Redis’s default port. -d runs it in the background. Connect to it with the bundled CLI:

docker exec -it redis redis-cli

and sanity-check the connection with the traditional Redis “are you alive” command:

PING

PONG

If you see PONG, the server is up and every example in this lecture will work against it.

2.2 Connecting from Python๐Ÿ”—

Install the official client:

pip install redis

and connect:

import redis

r = redis.Redis(
    host="localhost",
    port=6379,
    decode_responses=True,   # return Python str, not bytes
)

r.set("name", "Anwar")
print(r.get("name"))

Anwar

The decode_responses=True flag matters. By default the Python client returns raw bytes (Redis itself is byte-string-only under the hood); setting this flag decodes every response to a normal str so your application code doesn’t sprinkle .decode() calls everywhere. Every example in this lecture assumes it’s set.

Common misconception: “I need a database migration / schema before I can start.” Redis is schemaless — there is no CREATE TABLE. You just start writing keys. That’s part of its speed for prototyping, and part of its danger (§9): nothing stops a typo’d key name from silently creating a new, orphaned piece of state.

3. Redis Strings๐Ÿ”—

Strings are Redis’s simplest data type: a key maps to a single byte string (which can hold text, JSON, or a number Redis will treat specially for arithmetic commands).

3.1 Worked example: a user-profile cache field๐Ÿ”—

SET user:101:name Alice
GET user:101:name

"Alice"

Plain English. A string key is exactly the dict entry from §1.3 — one key, one value. The colon in user:101:name has no special meaning to Redis; it’s a convention (“namespacing”) that humans and tools use to group related keys and make them readable, the same way you’d name a variable user_101_name instead of x.

3.2 Worked example: atomic counters๐Ÿ”—

Redis strings that hold integers support atomic increment/decrement — useful for anything you’d otherwise read-modify-write:

SET page_views 0
INCR page_views
INCR page_views
GET page_views

2

Intuition — why “atomic” matters. If two requests both did value = GET counter; SET counter (value + 1) in your application code, a race between them could lose an increment (both read 5, both write 6, one increment vanishes). INCR does the read-modify-write inside Redis, as a single indivisible operation there is no window where two clients can interleave. This is the same class of bug you’ve seen with concurrent database writes, and the same class of fix (push the read-modify-write into the data store itself).

Common uses: page views, likes, API rate-limit counters (§9), “how many times has this student asked the AI assistant today.”

4. Expiration: Building the AI Answer Cache๐Ÿ”—

A cache that never forgets isn’t a cache — it’s a slowly growing memory leak that happens to answer requests. Redis’s answer is TTL (time to live): attach an expiration to a key, and Redis deletes it automatically when the clock runs out.

4.1 EX and TTL๐Ÿ”—

SET weather:fairfax "72F" EX 60
TTL weather:fairfax

45

EX 60 means “expire 60 seconds from now.” TTL reports the seconds remaining (here, we happened to check 15 seconds later). Once it hits zero, the key is simply gone — the next GET returns nothing, as if it had never been set.

Common misconception: “I should just delete keys myself when I’m done with them.” You could, but you’d have to remember to, from every code path, including the ones that crash. EX makes expiration a property of the data, not of your application’s control flow — it happens even if your server dies before it would have gotten around to cleaning up.

4.2 Worked example: caching the TerpTasks AI assistant๐Ÿ”—

Return to §1.1’s problem. Without a cache, every question re-runs the (here, simulated) LLM call:

answer = call_llm(question)   # slow + costs money, every single time

With a Redis cache in front of it:

import redis

r = redis.Redis(decode_responses=True)

def answer(question: str) -> str:
    cached = r.get(question)
    if cached is not None:
        print("Cache hit")
        return cached

    print("Calling AI...")
    result = call_llm(question)          # only runs on a miss
    r.set(question, result, ex=3600)     # remember it for one hour
    return result

Trace it exactly the way §8 of the React lecture traced a click: a student asks “What is RAG?” twice.

Call 1: answer("What is RAG?")

  1. r.get("What is RAG?")        → None            (miss)

  2. call_llm("What is RAG?")     → "AI answer for: What is RAG?"

  3. r.set(question, result, ex=3600)

  4. returns "AI answer for: What is RAG?"           (paid the LLM cost)

 

Call 2: answer("What is RAG?")   (asked again, 10 minutes later)

  1. r.get("What is RAG?")        → "AI answer for: What is RAG?"   (HIT)

  2. returns immediately                              (no LLM call at all)

Why ex=3600 and not forever? An LLM’s answer to a general question ("What is RAG?") is safe to cache for a long time. A question like “What’s due this week?” has an answer that goes stale the moment an assignment is added — caching it forever would serve students a wrong answer with total confidence. Choosing a TTL is choosing how stale you’re willing to be, in exchange for how much compute/money you save. There is no universally correct number; it’s a product decision, not a technical one.

Practical implication. Notice the cache key is the raw question text. That’s simple but fragile — “What is RAG?” and “what is rag” are different keys and both miss independently. A production system normalizes the key first (lowercase, strip punctuation, or hash a canonicalized prompt) before using it as the Redis key; Practice Exercise 6 asks you to fix exactly this.

5. Redis Lists: Queues๐Ÿ”—

A list is an ordered sequence of strings, efficient to push and pop from either end which makes it the natural structure for a queue.

5.1 Producer and consumer๐Ÿ”—

LPUSH jobs "process_file1"
LPUSH jobs "process_file2"

LPUSH pushes onto the left end, so the list now reads, left to right:

jobs:  process_file2   process_file1

A worker pulls from the right end with RPOP:

RPOP jobs

"process_file1"

Plain English. Push-left, pop-right (LPUSH/RPOP) gives you FIFO order — first job in, first job out — exactly like a line at a checkout counter. (Using LPUSH/LPOP together would give you a LIFO stack instead; the two ends are symmetric, and which pair you pick decides the ordering guarantee.)

r.lpush("tasks", "summarize_document")
task = r.rpop("tasks")
print(task)

summarize_document

5.2 The problem with RPOP: a worker that spins๐Ÿ”—

RPOP on an empty list returns immediately with nothing. A naive worker loop therefore looks like this:

while True:
    task = r.rpop("tasks")
    if task is None:
        time.sleep(0.1)   # poll again shortly
        continue
    process(task)

This works, but it’s polling: the worker asks Redis “anything for me?” ten times a second forever, even when the queue has been empty for a week. §6 introduces the fix.

6. Building a Background Job Queue๐Ÿ”—

This section builds the lecture’s second running example: instead of making a student wait while TerpTasks summarizes an uploaded PDF, we queue the work and let a separate worker process do it.

6.1 Why background work at all?๐Ÿ”—

User uploads syllabus.pdf

          

          

   Web Application  ──┐  if this thread does the summarizing,

                       the HTTP request hangs for however

    (must respond)      long extraction + the LLM call take

                     

     HTTP response ◄──┘

A web request should return in milliseconds. “Extract text, summarize, generate embeddings” can take seconds to minutes. The fix is the same producer/consumer split every real system uses (Celery, SQS, RabbitMQ all implement this same idea):

User uploads syllabus.pdf

          

          

   Web Application  ── LPUSH job onto queue ──► responds immediately: "queued"

                               

                               

                         Redis Queue

                                  

                                  

                     Worker 1   Worker 2   (separate processes, pull jobs)

The web server’s only job is to record the job and push it onto the queue; it never touches the slow work itself.

6.2 Job metadata: a Redis Hash๐Ÿ”—

A queue entry needs more than a filename — it needs a status a client can poll (“queued,” “processing,” “completed”). That’s a job for a hash: a Redis value that is itself a map from field names to values, exactly like one row of a table or one small JSON object.

HSET job:1001 filename lecture.pdf status queued
HGETALL job:1001

filename lecture.pdf
status queued

Plain English. Where a string key holds one value, a hash key holds a whole group of named fields under one key — the Redis equivalent of:

{"filename": "lecture.pdf", "status": "queued"}

From Python:

r.hset(
    "job:1001",
    mapping={"filename": "lecture.pdf", "status": "queued"},
)

6.3 BRPOP: a worker that waits instead of spins๐Ÿ”—

Redis has a blocking pop: BRPOP behaves exactly like RPOP, except that if the list is empty, the connection waits (blocks) until something arrives, instead of returning immediately.

job_id = r.brpop("job_queue", timeout=0)

timeout=0 means “wait forever.” This directly fixes §5.2’s spinning worker: no polling loop, no wasted CPU, no artificial 100ms delay before a new job is picked up. The worker process is simply asleep, parked inside the BRPOP call, until Redis wakes it the instant a job is pushed.

Common misconception: “BRPOP polls Redis under the hood, so it’s the same cost as my while loop.” It is not — the blocking is implemented on the server side of the connection; your client makes one call and the TCP connection simply doesn’t get a reply until there is data. Zero wasted round trips.

6.4 Putting it together: submit → queue → process → query๐Ÿ”—

import redis
import time
import uuid

r = redis.Redis(host="localhost", port=6379, decode_responses=True)


def submit_job(filename: str) -> str:
    """Producer: record the job, then queue it."""
    job_id = "job:" + str(uuid.uuid4())
    r.hset(job_id, mapping={"filename": filename, "status": "queued"})
    r.lpush("job_queue", job_id)
    return job_id


def process_job(job_id: str) -> None:
    """Consumer: do the (simulated) work, updating status before and after."""
    r.hset(job_id, "status", "processing")
    time.sleep(2)  # stand-in for extract text / summarize / embed
    r.hset(job_id, "status", "completed")


def worker() -> None:
    """Runs forever in its own process, waiting for jobs."""
    while True:
        _, job_id = r.brpop("job_queue")   # blocks until a job exists
        process_job(job_id)


def get_status(job_id: str) -> dict:
    return r.hgetall(job_id)

Run the producer in one terminal, the worker in another, and query status from a third — this is genuinely three separate OS processes cooperating through nothing but Redis:

Terminal 1 (producer)          Terminal 2 (worker)           Terminal 3 (status)

──────────────────────         ─────────────────────         ─────────────────────

>>> submit_job("lecture.pdf")  Worker started

'job:abc123'                   Processing job:abc123          >>> get_status("job:abc123")

                                Completed                      {'filename': 'lecture.pdf',

                                                                 'status': 'processing'}

                                                                (query again a moment later)

                                                                {'filename': 'lecture.pdf',

                                                                 'status': 'completed'}

Practical implication. Nothing here required the producer and the worker to be the same program, the same language, or even running on the same machine — they only need to agree on Redis’s address and the key naming scheme. This is exactly why real systems (Celery, for instance, uses Redis or RabbitMQ as its broker) build on this pattern: it decouples “accept the work” from “do the work” at the process boundary.

7. Redis Sets๐Ÿ”—

A set stores an unordered collection of unique strings — adding the same member twice is a no-op.

7.1 Worked example: deduplication๐Ÿ”—

SADD students Alice
SADD students Bob
SADD students Alice
SMEMBERS students

Alice
Bob

Alice only appears once, even though SADD was called with her name twice. Plain English: a set is Python’s set(), hosted in Redis and shared across processes.

7.2 Worked example: set algebra for recommendations๐Ÿ”—

Sets support the algebra you’d expect — intersection, union, difference — computed server-side in one call:

SADD cmsc330 Alice Bob Charlie
SADD cmsc433 Bob Charlie David

SINTER cmsc330 cmsc433

Bob
Charlie

Intuition. “Students in both cmsc330 and cmsc433” is exactly the set-theory intersection you’d draw as a Venn diagram — Redis just computes it without your application ever pulling both full lists across the network and diffing them in Python.

Practical implication. Common uses: tags (a post’s set of tags; SINTER finds posts sharing tags), permissions (a user’s set of roles; SISMEMBER checks membership in O(1)), and recommendations (“students who took a class you also took”).

8. Redis Hashes, Revisited๐Ÿ”—

§6.2 already introduced hashes for job metadata; here is the general picture. A hash maps a key to a set of field/value pairs — structurally, one row of a table, or one flat JSON object, stored under a single Redis key.

HSET user:1 name Alice age 25 major CS
HGETALL user:1

name Alice
age 25
major CS

Redis type

Shape

Analogy

String

one value

a single variable

List

ordered sequence

a Python list / a queue

Set

unordered, unique

a Python set

Hash

named fields → values

a Python dict / one DB row

Common misconception: “I’ll just json.dumps my object and store it as one string.” You can, and for small, rarely-updated objects it’s often simplest. But updating one field of a JSON string means fetching the whole string, parsing it in your application, changing one key, re-serializing, and writing the whole thing back — and if two clients do that concurrently, one update silently overwrites the other. A hash lets you do HSET job:1001 status completed as one atomic operation that touches only that field, which is exactly what §6.4’s process_job relies on.

9. Redis Persistence๐Ÿ”—

The question every student asks the moment they hear “in-memory”: if the Redis process restarts, is everything gone?

By default, yes — unless you turn on persistence. Redis offers two strategies, and understanding the trade-off between them is the point of this section.

9.1 RDB: periodic snapshots๐Ÿ”—

RDB (Redis Database file) periodically writes the entire in-memory dataset to a single compact file on disk.

Memory (all keys) ── every few minutes ──►  dump.rdb  (a full snapshot)

Trade-off. Fast to restart from (load one file), small on disk, but you can lose everything written since the last snapshot if the snapshot runs every 5 minutes and the process crashes 4 minutes after one, those 4 minutes of writes are gone.

9.2 AOF: an append-only log of every write๐Ÿ”—

AOF (Append-Only File) logs every write command as it happens, in order:

SET x 10
INCR x
SET y hello

To recover, Redis simply replays the log from the start. Configure how often the log is flushed to disk with appendfsync:

appendfsync everysec

everysec (the recommended default) flushes once per second — so a crash loses at most about one second of writes, at a small continuous disk-I/O cost. (always flushes every single write for near-zero data loss at real latency cost; no lets the OS decide, for best performance and the weakest guarantee.)

RDB

AOF

What it stores

Periodic full snapshot

Every write command, in order

Worst-case data loss

Since the last snapshot (minutes)

≈1 second with everysec

Restart speed

Fast (load one file)

Slower (replay the log)

Disk size

Compact

Grows continuously (Redis compacts it periodically)

Practical implication. Production Redis often runs both: RDB for fast, cheap backups/restarts, AOF for the tight durability window. But notice the recurring theme of this whole lecture: if a piece of data would be a disaster to lose even for one second, it doesn’t belong in Redis at all — it belongs in Postgres, which is built around exactly that guarantee (transactions, write-ahead logging, replication) from the ground up.

10. Redis in Modern AI Systems๐Ÿ”—

Zoom out to where everything in this lecture fits in a realistic AI application architecture:

                    User

                     

                     

               Application  (TerpTasks web server)

                     

      ┌──────────────┼──────────────┐

                                  

Redis Cache     Job Queue      Vector Database

(§4: answers)   (§6: async     (semantic search over

(rate limits)    document       task descriptions,

(sessions)       processing)    for retrieval)

                                  

      └──────────────┼──────────────┘

                     

                    LLM

Redis alone typically covers all of the following in an AI stack:

  • LLM response cache (§4) — don’t pay for the same generation twice.

  • Conversation history a list per session of the last N turns, so the LLM has context without a database round trip per message.

  • User sessions a hash per logged-in user with a TTL, so a session dies automatically after inactivity (Practice Exercise 8).

  • Rate limiting an INCR-with-EX counter per user per minute (§3.2), so one user can’t monopolize an expensive LLM endpoint.

  • Task queues (§6) — the async work: embeddings, summaries, document extraction.

Notice what is not on this list: the vector database and the system-of-record data (the actual Task rows). That’s the boundary this whole lecture has been drawing — Redis owns fast, ephemeral, recomputable state; a real database owns the truth.

11. The Complete Project: terptasks-redis๐Ÿ”—

Everything above is implemented, runnable, and tested in a small companion project. It extends TerpTasks with exactly the two features this lecture built: an AI answer cache and a document-processing job queue.

11.1 Repository structure๐Ÿ”—

terptasks-redis/

├── requirements.txt          # redis, pytest, fakeredis

├── README.md

├── docker-compose.yml        # spins up Redis for local dev

└── app/

    ├── __init__.py

    ├── cache.py               # AI answer cache                       (§4)

    ├── queue.py                # submit_job / worker / get_status      (§6)

    ├── stats.py                # INCR-based counters                   (§3.2, §9)

    └── cli.py                  # tiny command-line demo of all three

└── tests/

    ├── conftest.py             # fakeredis fixture — no real server needed

    ├── test_cache.py

    └── test_queue.py

11.2 docker-compose.yml๐Ÿ”—

services:
  redis:
    image: redis:7
    ports:
      - "6379:6379"

docker compose up -d

11.3 app/cache.py the AI answer cache๐Ÿ”—

"""AI answer cache for TerpTasks (see lecture ยง4)."""

import redis

r = redis.Redis(host="localhost", port=6379, decode_responses=True)

CACHE_TTL_SECONDS = 300  # 5 minutes, per the exercise spec


def _fake_llm(question: str) -> str:
    """Stand-in for a real LLM call โ€” replace with an actual API call."""
    return "AI answer for: " + question


def answer(question: str) -> str:
    from app.stats import record_question, record_hit, record_miss

    record_question()

    cached = r.get(question)
    if cached is not None:
        record_hit()
        return cached

    record_miss()
    result = _fake_llm(question)
    r.set(question, result, ex=CACHE_TTL_SECONDS)
    return result

11.4 app/queue.py the job queue๐Ÿ”—

"""Background job queue for TerpTasks document processing (see lecture ยง6)."""

import time
import uuid

import redis

r = redis.Redis(host="localhost", port=6379, decode_responses=True)

QUEUE_KEY = "job_queue"


def submit_job(filename: str) -> str:
    job_id = "job:" + str(uuid.uuid4())
    r.hset(job_id, mapping={"filename": filename, "status": "queued"})
    r.lpush(QUEUE_KEY, job_id)
    return job_id


def process_job(job_id: str) -> None:
    r.hset(job_id, "status", "processing")
    time.sleep(1)  # simulate extract โ†’ summarize โ†’ embed
    r.hset(job_id, "status", "completed")


def worker(run_once: bool = False) -> None:
    """`run_once=True` lets tests exercise one iteration without blocking forever."""
    while True:
        popped = r.brpop(QUEUE_KEY, timeout=0 if not run_once else 1)
        if popped is None:
            return
        _, job_id = popped
        process_job(job_id)
        if run_once:
            return


def get_status(job_id: str) -> dict:
    return r.hgetall(job_id)

11.5 app/stats.py counters๐Ÿ”—

"""Statistics counters (see lecture ยง3.2 and ยง9)."""

import redis

r = redis.Redis(host="localhost", port=6379, decode_responses=True)


def record_question() -> None:
    r.incr("total_questions")


def record_hit() -> None:
    r.incr("cache_hits")


def record_miss() -> None:
    r.incr("cache_misses")


def get_stats() -> dict:
    return {
        "total_questions": int(r.get("total_questions") or 0),
        "cache_hits": int(r.get("cache_hits") or 0),
        "cache_misses": int(r.get("cache_misses") or 0),
    }

11.6 Testing without a real Redis server๐Ÿ”—

Just as the FastAPI/React lectures fake the network boundary in unit tests (a stubbed fetch, a FakeClient), Redis code is tested against fakeredis an in-memory drop-in that speaks the same Python client API without needing a real server running in CI.

# tests/conftest.py
import fakeredis
import pytest


@pytest.fixture(autouse=True)
def fake_redis(monkeypatch):
    """Replace every module's `r` with one shared in-memory fake Redis."""
    fake = fakeredis.FakeStrictRedis(decode_responses=True)
    monkeypatch.setattr("app.cache.r", fake)
    monkeypatch.setattr("app.queue.r", fake)
    monkeypatch.setattr("app.stats.r", fake)
    yield fake

# tests/test_cache.py
from app.cache import answer
from app.stats import get_stats


def test_second_identical_question_is_a_cache_hit():
    first = answer("What is RAG?")
    second = answer("What is RAG?")

    assert first == second == "AI answer for: What is RAG?"
    assert get_stats() == {
        "total_questions": 2,
        "cache_hits": 1,
        "cache_misses": 1,
    }

# tests/test_queue.py
from app.queue import submit_job, worker, get_status


def test_submitted_job_is_completed_after_one_worker_pass():
    job_id = submit_job("lecture.pdf")
    assert get_status(job_id)["status"] == "queued"

    worker(run_once=True)

    assert get_status(job_id)["status"] == "completed"

pytest -v

tests/test_cache.py::test_second_identical_question_is_a_cache_hit PASSED
tests/test_queue.py::test_submitted_job_is_completed_after_one_worker_pass PASSED

Why fake, not real, Redis in tests? The same reason every earlier lecture faked its I/O boundary: tests that need a live server are slow, flaky in CI, and require infrastructure just to run pytest. fakeredis implements the real command semantics in memory, so the logic under test (cache-hit bookkeeping, status transitions) is verified exactly, with zero setup.

11.7 Running the demo end to end๐Ÿ”—

docker compose up -d          # start real Redis
python -m app.cli

Question: What is RAG?

Calling AI...

AI answer for: What is RAG?

 

Question: What is RAG?

Cache hit

AI answer for: What is RAG?

 

Queued: document1.pdf, document2.pdf

Worker: Processing document1.pdf... completed

 

Statistics

Questions: 2

Cache hits: 1

Cache misses: 1

Practice Exercises๐Ÿ”—

Basic๐Ÿ”—
  1. Using redis-cli, SET a key with a 30-second TTL, run TTL on it immediately and again after 35 seconds, and explain the two different outputs you see.

  2. Explain, in your own words, why INCR needs to be atomic and what could go wrong if an application implemented the same counter as value = int(r.get(k)); r.set(k, value + 1) instead.

  3. Given the hash job:2001 with fields filename=paper.pdf and status=queued, write the single Redis command that updates only its status to processing without touching filename.

  4. Two sets, cmsc330 and cmsc433, each list student names. Write the command that finds students in cmsc330 but not in cmsc433. (Hint: look up SDIFF.)

  5. A teammate says “let’s just store the whole Task object as one JSON string in a Redis string key instead of using a hash.” Give one concrete scenario from §8 where this choice causes a bug their approach wouldn’t have.

Intermediate๐Ÿ”—
  • 6. Fix the cache key. §4.2’s cache uses the raw question text as the Redis key, so “What is RAG?” and “what is rag” miss independently. Normalize the key (lowercase + strip punctuation, or hash the normalized text) before using it in answer(), and write a test proving both spellings now hit the same cache entry.

  • 7. Expiring sessions. Store a session as a hash session:<user_id> with fields like logged_in_at, and set a 30-minute TTL with EXPIRE. Write a function touch_session(user_id) that resets the TTL back to 30 minutes on every request (so an active user is never logged out), and explain why EXPIRE on every request, rather than TTL once, is the right approach.

  • 8. Leaderboard with sorted sets. Redis has a fifth data structure not covered in lecture: the sorted set (ZADD, ZRANGE), which keeps members ordered by a numeric score. Build a small “AI assistant leaderboard” tracking how many helpful answers each student has rated, using ZADD scores 100 Alice / ZINCRBY, and retrieve the top 3 with ZREVRANGE.

  • 9. Priority queue. §6’s job queue processes strictly FIFO. Rebuild it as a priority queue using a sorted set instead of a list (urgent jobs get a higher score), so an urgent document is always processed before a normal one submitted earlier. What does the worker’s pop operation become, and why can’t you use BRPOP anymore?

  • 10. Failed jobs and retries. Extend process_job to randomly fail 10% of the time (simulate an extraction error), set status=failed and a retries field, and have the worker re-queue a failed job up to 2 times before giving up and setting status=permanently_failed. Track a jobs_failed counter alongside jobs_completed.

Advanced๐Ÿ”—
  • 11. Rate limit the AI endpoint. Using only INCR and EXPIRE, implement allow_request(user_id, limit=5, window=60) that returns False once a user has made more than 5 requests in the current 60-second window, and True otherwise. Handle the edge case of the first request in a window carefully (the key doesn’t exist yet — when do you set its expiration, and what happens if you set it on every call instead of only the first?).

  • 12. Real RAG integration. Replace _fake_llm in app/cache.py with a real pipeline: embed the question, query a vector database (or an in-memory stand-in) for the most relevant TerpTasks records, and pass them plus the question to a real LLM API. Keep the Redis caching layer exactly as-is. What does this tell you about where a cache belongs relative to the “smart” part of a system?

  • 13. Design for a crash mid-job. §6’s worker sets status=processing then does the work then sets status=completed. If the worker process is killed during the work, the job is stuck at processing forever, and no other worker will ever pick it up (it already left the queue). Design a fix using a combination of a per-job TTL/heartbeat and a periodic “reaper” process, and explain the trade-off between checking too often (wasted work) and too rarely (jobs stuck for a long time before being retried).

Summary๐Ÿ”—

Key takeaways๐Ÿ”—
  1. Redis is an in-memory key-value store with rich data structures strings, lists, sets, and hashes cover the overwhelming majority of real use cases, and each maps to a familiar Python type (value, list, set, dict).

  2. A cache without expiration is a memory leak. Always attach a TTL (EX/EXPIRE) to cache entries, and choose its length as a product decision about acceptable staleness, not a technical afterthought.

  3. Atomic operations (INCR, single-field HSET) avoid the read-modify-write race that plain application-level counters are prone to.

  4. Lists plus BRPOP give you a producer/consumer job queue for free a web request pushes work and returns immediately; a separate worker process blocks efficiently until work exists, instead of polling.

  5. Hashes track structured, frequently-updated state (like a job’s status) without the read-whole-object-to-change-one-field trap of storing JSON in a plain string.

  6. RDB and AOF trade restart speed against data-loss window neither makes Redis a system of record; both exist to make Redis’s cache and queue state survive an ordinary restart.

  7. Redis owns fast, ephemeral, recomputable state; a real database owns the truth. Every design decision in this lecture — what goes in Redis versus Postgres, how long a TTL should be, whether a crash losing a few seconds of writes is acceptable — comes back to that one line.

Terminology๐Ÿ”—

Term

Definition

Key-value store

A database where every value is looked up by a single key, with no query language over the value’s internals

TTL

Time-to-live: seconds remaining before Redis auto-deletes a key

Cache hit / miss

The requested value was / was not already stored when looked up

Atomic operation

A command Redis executes as one indivisible step, with no interleaving from other clients

Producer / consumer

One process creates units of work; a separate process (or pool) executes them

Blocking pop (BRPOP)

A pop that waits for an item to exist instead of returning empty immediately

RDB

Point-in-time snapshot persistence: periodic full dump to disk

AOF

Append-only log persistence: every write command recorded, replayed on restart

System of record

The authoritative, durable store for data that cannot be regenerated if lost

Common mistakes๐Ÿ”—
  • Setting a key with no TTL for data that’s meant to be a temporary cache, so Redis’s memory grows without bound.

  • Implementing a counter as application-level read-then-write instead of INCR, introducing a race condition under concurrent requests.

  • Storing an entire object as one JSON string, then needing a read-modify-write round trip (with a race window) to update a single field — when a hash would update that field atomically.

  • Writing a worker loop around plain RPOP with a sleep(), polling Redis continuously instead of using BRPOP.

  • Treating Redis as the permanent record for data that would be a disaster to lose, instead of keeping the source of truth in a real database and using Redis only for what’s cheap to recompute.

  • Using raw, un-normalized user input as a cache key, so trivially different inputs (case, whitespace, punctuation) miss the cache independently.

  • Forgetting that a worker crash mid-job leaves that job’s status stuck forever, with nothing to notice or retry it (Advanced Exercise 13).

Connections๐Ÿ”—
  • Backward: the REST/FastAPI lecture’s TerpTasks is exactly what this lecture adds a cache and a job queue to; the layered-architecture lecture’s “isolate the cross-cutting concern in one layer” is why cache.py/queue.py/stats.py are separate modules rather than inline in route handlers; the testing lecture’s “fake the boundary” philosophy reappears as fakeredis standing in for a live server.

  • Forward: the vector-database and RAG lectures fill in _fake_llm’s replacement with real retrieval (Advanced Exercise 12); the deployment lecture covers running Redis (and a worker process) as separate services alongside the API in production; message brokers like Celery, RabbitMQ, and SQS generalize §6’s producer/consumer pattern beyond what a single Redis list can express (priority, delayed delivery, at-least-once acknowledgment).

Instructor Notes๐Ÿ”—

Not part of the student-facing notes — for whoever is teaching this lecture.

Suggested duration. 2–3 hours total: roughly 45 minutes for §1–4 (motivation through the cache), 45 minutes for §5–6 (lists through the job queue — this is the section students find least intuitive and benefits most from a live demo across two terminals), 20 minutes for §7–9 (sets, hashes, persistence), 15 minutes for §10–11 (AI architecture and the complete project walkthrough), leaving the rest of the block for exercises.

Where students commonly struggle.

  • LPUSH/RPOP ordering. Which end is “first out” is genuinely confusing on first exposure; draw the list as a physical queue on the board (§5.1’s ASCII diagram) rather than describing it verbally.

  • Why BRPOP matters. Students who haven’t hit a spinning poll loop before don’t feel the pain §5.2/§6.3 are solving. If time allows, have them run the naive while True: RPOP(); sleep() version and watch CPU usage, then swap to BRPOP and watch it drop to zero.

  • TTL vs. deleting yourself. Some students reach for manual cleanup code before being shown EX/EXPIRE; reinforce that expiration-as-data-property is the whole point.

  • “Isn’t this just a dict?” Yes, and that’s the right intuition for a single process — the lecture’s job is to make clear why pulling that dict out into its own shared, network-reachable process (multiple workers, a restart-safe queue) is worth the trade-off.

Suggested live demonstrations.

  • Open two terminal windows side by side: run the §6.4 producer in one and the worker in the other, submit a job, and watch the worker wake up instantly on BRPOP with no polling delay.

  • redis-cli MONITOR in a third terminal while running the Python client examples — students seeing the actual wire commands stream by demystifies “what is the Python library actually doing.”

  • Kill the Redis container mid-demo (with no persistence configured), restart it, and show that a plain in-memory cache is gone — then repeat with AOF enabled and show the data survives.

Suggested quiz questions.

  • Why does INCR need to be atomic? Give an interleaving of two clients that would produce a wrong count without it.

  • A key is set with SET k v EX 60. A client calls TTL k at the 65-second mark. What does it return, and why?

  • Explain the difference in worst-case data loss between RDB and AOF with appendfsync everysec.

  • Why is BRPOP preferable to a while True: RPOP(); sleep() loop for a queue worker?

  • Give one example of data that belongs in Redis and one example of data that must live in a system-of-record database, and justify each.

Suggested homework. The Final Exercise structure in notes/redis/plan.md (LLM cache + task queue + statistics, with optional extensions for expiring sessions, a leaderboard via sorted sets, and real RAG integration) and the standalone job-processing exercise in notes/redis/exercise_plan.md both map directly onto §11’s terptasks-redis project scaffold and the Practice Exercises above — assign the Basic tier as a checkpoint, Intermediate as the graded homework, and Advanced as optional extra credit for students headed toward the AI frameworks / RAG lectures.