Technology

Redis Tutorial: Caching Patterns & Real-World Use Cases

A hands-on Next.js tutorial app for learning Redis database caching patterns and real-world use cases. The app uses an in-memory SQLite database as the primary data store and Redis as the caching layer, letting you observe cache hits, misses, and invalidation in real time.

30 min read
Published

Complete Tutorial Code

Follow along with the complete source code for this Redis tutorial. Includes four interactive tabs covering basic caching, advanced caching techniques, and real-world production patterns.

View on GitHub

Introduction

Redis is one of the most battle-tested tools in distributed systems engineering. Whether you're building a startup side project or scaling to millions of requests per second, Redis shows up as the go-to solution for caching, session storage, rate limiting, leaderboards, pub/sub messaging, and more.

This tutorial takes a hands-on approach — each tab in the app corresponds to a progressively more advanced Redis use case, letting you observe cache hits, misses, TTL expiry, and atomic operations in real time. You'll start with the raw SQLite data, then layer on basic caching patterns, advanced techniques, and finally production-grade Redis patterns used by real companies.

Why Redis?

Redis is one of the most widely adopted tools in distributed systems. It shows up everywhere — from startup side projects to systems handling millions of requests per second at Twitter, GitHub, Snapchat, and Stack Overflow. Understanding Redis is a core skill for any backend engineer.

Blazing Fast

Redis stores all data in memory, making reads and writes orders of magnitude faster than a traditional disk-based database. Sub-millisecond response times are the norm.

Shared Cache Across Servers

In a distributed system with dozens of application servers behind a load balancer, Redis acts as a single shared cache that every server reads from and writes to — keeping data consistent across the fleet.

Reduces Database Load

Databases are often the bottleneck in high-traffic systems. By serving repeated reads from Redis instead of hitting the database every time, you can handle far more traffic without scaling up your database.

Atomic Operations

Redis executes every command atomically. In a distributed system where dozens of servers hit the cache simultaneously, atomicity guarantees that operations like INCR, SETNX, and ZADD complete as a single, indivisible step — no race conditions.

Built-in TTL

Redis lets you set an expiry on any key. Cached data automatically disappears after a set time, so you don't have to manually clean up stale entries.

Versatile Data Structures

Beyond simple key-value strings, Redis supports Hashes, Lists, Sets, Sorted Sets, and more — making it useful for caching, session storage, leaderboards, pub/sub messaging, rate limiting, and queues, all in one tool.

Prerequisites

You only need two things to run this tutorial:

  • Node.js (v18 or later)
  • Docker — to run Redis as a container (no local Redis installation required)

Getting Started

This app connects to Redis on localhost:6379. The easiest way to get Redis running is with Docker — no installation required.

  1. 1
    Start Redis Stack as a Docker container:
    docker run -d --name redis-tutorial -p 6379:6379 -p 8001:8001 redis/redis-stack:latest

    Redis Stack bundles Redis with Redis Insight (a visual browser UI) in a single container. Open http://localhost:8001 to browse keys, inspect values, and watch TTLs count down in real time.

    FlagMeaning
    -dRun the container in the background (detached mode)
    --name redis-tutorialGive the container a friendly name so you can reference it easily
    -p 6379:6379Redis port — used by the app
    -p 8001:8001Redis Insight UI port — open in your browser
    redis/redis-stack:latestOfficial Redis Stack image (includes Redis + Redis Insight)

    Optional — connect to the Redis CLI inside the container:

    docker exec -it redis-tutorial redis-cli

    From here you can inspect keys in real time (e.g. KEYS *, TTL query:inventory, GET query:inventory). Type exit to leave the CLI.

  2. 2
    Clone the repository:
    git clone https://github.com/audoir/redis-tutorial.git
  3. 3
    Install dependencies:
    npm install
  4. 4
    Run the development server:
    npm run dev

    Open http://localhost:3000 in your browser.

Project Structure

The project is organized around four tabs, each backed by dedicated API routes and UI components. Work through them in order for the best learning experience — each one builds on the concepts introduced before it.

redis-tutorial/
├── app/
│   ├── api/
│   │   ├── database/          # Serves raw SQLite data to the View Database tab
│   │   ├── basic-cache/       # Cache-Aside and Write-Through API routes
│   │   ├── advanced-cache/    # Advanced caching technique API routes
│   │   │   └── techniques/    # One file per caching technique
│   │   ├── real-world/        # Real-world use case API routes
│   │   │   └── use-cases/     # One file per use case (GET + POST handlers)
│   │   └── redis-status/      # Redis connection health check
│   ├── components/
│   │   ├── PageHeader.tsx               # App header with Redis connection status
│   │   ├── TabNavigation.tsx            # Top-level tab bar
│   │   ├── DatabaseView.tsx             # Tab 1 — View Database
│   │   ├── BasicDatabaseCaching/        # Tab 2 — Basic Caching
│   │   ├── AdvancedDatabaseCaching/     # Tab 3 — Advanced Caching
│   │   ├── RealWorldUseCases/           # Tab 4 — Real World Use Cases
│   │   │   └── panels/                 # Per-use-case control panels
│   │   └── shared/CachingShared.tsx     # Shared UI components
│   └── page.tsx               # Root page with tab navigation
├── lib/
│   ├── db.ts                  # SQLite (better-sqlite3) setup & seed data
│   ├── redis.ts               # Redis (ioredis) client
│   └── types.ts               # Shared TypeScript types
└── README.md
1

🗄️ Tab 1 — View Database

A live view of the underlying SQLite database. Browse the inventory (10 products), customers (8 records), and sales (20 records with joins) tables. The SQL query being executed is shown above each table — this is the exact query that Redis will cache in the next tabs.

SQLitebetter-sqlite3Live Data
2

⚡ Tab 2 — Basic Database Caching

The two most fundamental Redis caching patterns: Cache-Aside (lazy loading) and Write-Through (proactive caching). Watch cache hits and misses in the Activity Log and observe the key difference between reactive and proactive caching.

Cache-AsideWrite-ThroughTTLCache Invalidation
3

🚀 Tab 3 — Advanced Database Caching

Six advanced caching techniques including the four relational database caching strategies from the AWS Database Caching Strategies whitepaper, plus two bonus aggregate-query patterns. Each technique uses a different Redis data structure and cache key strategy.

SQL ResultSetJSON FieldsRedis HashSerialized ObjectAggregationJOIN + GROUP BY
4

🌍 Tab 4 — Real World Use Cases

Seven production-grade Redis patterns that power real applications. Each use case highlights a different Redis data structure and command set — from API caching and session management to distributed locking and batch write buffers.

API CachingSessionsRate LimitingLeaderboardPub/SubDistributed LockBatch Write

🗄️ Tab 1 — View Database

This tab gives you a live view of the underlying SQLite database. Use it to understand the data you'll be caching throughout the tutorial.

What to do

  1. 1. Open the View Database tab.
  2. 2. Click the inventory button to see all products (10 rows).
  3. 3. Click customers to see the 8 customer records.
  4. 4. Click sales to see the 20 sales records — notice this view joins inventory and customers to show product names and customer names alongside each sale.
  5. 5. Click Refresh at any time to re-fetch the data from SQLite.

The SQL query being executed is shown in the dark code block above each table — this is the exact query that Redis will cache in the next tabs.

-- Example: inventory query
SELECT * FROM inventory;

-- Example: sales query (with joins)
SELECT s.id, s.quantity, s.sale_date,
       i.name AS product_name, i.price,
       c.name AS customer_name, c.city
FROM sales s
JOIN inventory i ON s.product_id = i.id
JOIN customers c ON s.customer_id = c.id
ORDER BY s.sale_date DESC;

⚡ Tab 2 — Basic Database Caching

This tab demonstrates the two most fundamental Redis caching patterns: Cache-Aside and Write-Through.

🔵 Pattern A — Cache-Aside (Lazy Loading)

Cache-Aside is a reactive pattern. The cache is only populated when data is actually requested. The app checks Redis first; on a cache miss it queries SQLite, stores the result in Redis with a 60-second TTL, then returns the data.

// Cache-Aside flow
1. Check Redis: GET query:inventory
2. Cache HIT  → return data immediately (fast ⚡)
3. Cache MISS → query SQLite
              → SET query:inventory <result> EX 60
              → return data

Try it

  1. 1. Select 🔵 Cache-Aside (it's selected by default).
  2. 2. Choose a table — start with inventory.
  3. 3. Click Read Query.
    • The Activity Log will show an orange Cache MISS entry — Redis had no data yet, so SQLite was queried and the result was cached.
  4. 4. Click Read Query again immediately.
    • This time you'll see a green Cache HIT — the data came from Redis, not SQLite.
  5. 5. Wait 60 seconds (or click Invalidate Cache), then click Read Query once more.
    • You'll get another Cache MISS as the TTL has expired and the cache is empty again.
  6. 6. Repeat with the customers and sales tables to see separate cache keys (query:customers, query:sales).

🟣 Pattern B — Write-Through

Write-Through is a proactive pattern. The cache is updated at the same time as the database, so reads almost always find data in the cache. With Write-Through, the very first read after a write is always a cache hit — unlike Cache-Aside where the first read after a miss always hits the database.

// Write-Through flow
1. Write to SQLite (UPDATE / INSERT)
2. Immediately refresh Redis: SET query:inventory <result> EX 60
3. Next read → Cache HIT (cache was pre-populated by the write)

Try it

  1. 1. Select 🟣 Write-Through.
  2. 2. Choose a table (e.g. inventory).
  3. 3. Click Simulate DB Write.
    • The Activity Log shows a purple Write entry — the database was updated and the cache was refreshed immediately.
  4. 4. Click Read Query.
    • You'll get a green Cache HIT right away, because the cache was pre-populated by the write.
  5. 5. Compare this to Cache-Aside: with Write-Through, the very first read after a write is always a cache hit.

Key difference to observe: In Cache-Aside, the first read after a cache miss (or invalidation) always hits the database. In Write-Through, the cache is kept in sync proactively, so reads are almost always served from Redis.

🚀 Tab 3 — Advanced Database Caching

This tab explores six advanced caching techniques, including the four relational database caching strategies from the AWS Database Caching Strategies whitepaper, plus two bonus aggregate-query patterns. All techniques use a 60-second TTL. Select a technique, run a query, and watch the Activity Log.

📋 Technique 1 — Cache SQL ResultSet

Redis type: StringKey: sql:<base64(SQL)>

The SQL query string itself is used as the cache key (base64-encoded). The full result set is serialized and stored as a single Redis string. Ideal when the same query is repeated frequently with the same parameters.

Try it
  1. 1. Select 📋 Cache SQL ResultSet.
  2. 2. Pick a Customer ID (e.g. #1 for Alice).
  3. 3. Click Run Query — first call is a Cache MISS; the SQL result is fetched from SQLite and stored in Redis.
  4. 4. Click Run Query again — Cache HIT, data served from Redis.
  5. 5. Click Invalidate Cache to delete the key, then run again to see the miss/populate cycle repeat.

📄 Technique 2 — Cache Fields as JSON

Redis type: String (JSON)Key: customer:id:<id>

Only a selected subset of customer fields is serialized as a JSON string and stored in Redis — not the entire row. Useful when your app only needs specific fields and you want to minimize cache size.

Try it
  1. 1. Select 📄 Cache Fields as JSON.
  2. 2. Choose a Customer ID.
  3. 3. Click Run Query — on a miss, the selected fields are fetched from SQLite, serialized to JSON, and stored in Redis.
  4. 4. Click Run Query again — Cache HIT, the JSON string is parsed and returned.
  5. 5. Try different Customer IDs — each gets its own cache key (customer:id:1, customer:id:2, etc.).

🗂️ Technique 3 — Cache into Redis Hash

Redis type: Hash (HSET / HGETALL)Key: customer:hash:<id>

Each customer field is stored as a separate field in a Redis Hash. This allows you to read or update individual fields without deserializing the whole object — a key advantage over the JSON string approach.

Try it
  1. 1. Select 🗂️ Cache into Redis Hash.
  2. 2. Choose a Customer ID.
  3. 3. Click Run Query — on a miss, each field is written to the Redis Hash with HSET; on a hit, all fields are retrieved with HGETALL.
  4. 4. Use the Hash — Patch a Single Field section: set field to city and value to Las Vegas, then click HSET Field. Only that one field is updated in Redis.
  5. 5. Compare to the JSON technique: with JSON you'd have to rewrite the entire string to change one field; with a Hash you update only what changed.

🧩 Technique 4 — Cache Serialized Object

Redis type: String (JSON)Key: customer:object:<id>

A rich application-level object is constructed (including computed fields like totalSpent and recentPurchases) and serialized as a single JSON string. The most feature-rich caching approach — derived fields are computed once at cache-population time.

Try it
  1. 1. Select 🧩 Cache Serialized Object.
  2. 2. Choose a Customer ID.
  3. 3. Click Run Query — on a miss, the app joins multiple tables to build the enriched object, then caches it.
  4. 4. Click Run Query again — Cache HIT; the full enriched object is returned from Redis with no database queries.
  5. 5. Notice the result includes computed fields that don't exist as raw columns in SQLite — these are derived at cache-population time.

🏆 Technique 5 — Cache Aggregated Query (Leaderboard)

Redis type: String (JSON array)Key: leaderboard:top-products

An expensive GROUP BY aggregation query (top-selling products by revenue) is cached as a single key shared by all users. Ideal for dashboards and leaderboards where the same expensive query is run by many users.

Try it
  1. 1. Select 🏆 Cache Aggregated Query.
  2. 2. Click Run Query — on a miss, the GROUP BY query runs against SQLite and the result is cached.
  3. 3. Click Run Query again — Cache HIT; the leaderboard is served instantly from Redis.
  4. 4. Notice there is no Customer ID selector — this is a global cache key, not per-user.
  5. 5. Click Invalidate Cache to clear it, then run again to see the aggregation re-execute.

🌆 Technique 6 — Cache JOIN + GROUP BY

Redis type: String (JSON array)Key: aggregate:city-summary

The most expensive query type — a multi-table JOIN combined with a GROUP BY — is cached as a single result set. This technique shows the biggest performance difference between a cache miss and a cache hit.

Try it
  1. 1. Select 🌆 Cache JOIN + GROUP BY.
  2. 2. Click Run Query — on a miss, the join + aggregation runs against SQLite and the result is cached.
  3. 3. Click Run Query again — Cache HIT; the full report is returned from Redis with zero database work.
  4. 4. This technique shows the biggest performance difference between a cache miss and a cache hit, since the underlying query is the most complex.

🌍 Tab 4 — Real World Use Cases

This tab goes beyond database caching and demonstrates seven production-grade Redis patterns that power real applications. Each use case highlights a different Redis data structure and command set.

1

🌐 API Response Caching

String (JSON blob)api-cache:<endpoint>

External API calls can be slow (100–500ms) and expensive (rate-limited or billed per request). Redis caches the response so subsequent callers get sub-millisecond reads. Choose from three simulated endpoints and configurable TTLs.

Try it
  1. 1. Select 🌐 API Response Caching.
  2. 2. Choose an Endpoint/api/products, /api/weather, or /api/exchange-rates.
  3. 3. Choose a Cache TTL (10s, 30s, or 60s).
  4. 4. Click Fetch (Cache-Aside) — on a miss, the upstream API is simulated (~120–200ms latency) and the response is cached. On a hit, the response is served instantly from Redis.
  5. 5. Click Invalidate Cache to delete the key, then fetch again to see the miss/populate cycle.
2

🔐 Session Management

Hash (HSET / HGETALL)session:<sessionId>

Sessions need fast reads on every authenticated request. Redis Hashes store structured session data with per-field updates and automatic TTL-based expiry — no cron job or database polling needed. Demonstrates login, refresh, and logout flows.

Try it
  1. 1. Select 🔐 Session Management.
  2. 2. Choose a User ID (1–8, each maps to a unique session ID like sess_alice_001).
  3. 3. Choose a TTL (60s, 120s, or 300s).
  4. 4. Click Login (Create Session) — the user's data is fetched from SQLite and stored as a Redis Hash with HSET.
  5. 5. Click Get Session — retrieves the session with HGETALL.
  6. 6. Click Refresh Session — resets the TTL without changing the session data.
  7. 7. Click Logout (Destroy) — deletes the session key from Redis immediately.
3

🚦 Rate Limiting

String (INCR)Sorted Set (ZADD)Token Bucket

Redis is ideal for rate limiting because its operations are atomic — no race conditions. Three algorithms are demonstrated: Fixed Window (simple, fast), Sliding Window (accurate, no boundary bursts), and Token Bucket (allows short bursts up to capacity).

Try it
  1. 1. Select 🚦 Rate Limiting.
  2. 2. Choose an Algorithm (Fixed Window, Sliding Window, or Token Bucket).
  3. 3. Choose a User ID and set Max Requests and Window.
  4. 4. Click Send Request repeatedly — watch the Activity Log turn green (allowed) then red (blocked) once the limit is hit.
  5. 5. Switch between algorithms to compare their behavior — notice how Fixed Window can allow a burst at the window boundary, while Sliding Window prevents this.
4

Real-Time Leaderboard

Sorted Set (ZADD / ZREVRANGE / ZINCRBY)leaderboard:game-scores

Redis Sorted Sets maintain a ranked list automatically. ZADD updates a score in O(log N), ZREVRANGE retrieves the top-N in O(log N + N). No SQL GROUP BY or ORDER BY needed.

Try it
  1. 1. Select 🏆 Real-Time Leaderboard.
  2. 2. Click Seed Players — adds 10 players with initial scores using ZADD.
  3. 3. Click View Leaderboard — retrieves the top 10 players using ZREVRANGE WITHSCORES.
  4. 4. Choose a Player and an Increment, then click +N (ZINCRBY) — atomically increments the player's score.
  5. 5. Click Clear to delete the leaderboard and start over.
5

Pub/Sub & Event Log

Pub/Sub (PUBLISH)List (LPUSH / LRANGE)pubsub:event-log

Redis PUBLISH broadcasts messages to all subscribers in real time. A Redis List acts as a ring buffer for the event log — LPUSH prepends new events, LTRIM caps the list at 50 entries, and LRANGE reads them back.

Try it
  1. 1. Select 📡 Pub/Sub & Event Log.
  2. 2. Choose a Channel (notifications, orders, inventory, or analytics).
  3. 3. Choose an Event Type and click Publish Event — the event is published with PUBLISH and appended to the log with LPUSH.
  4. 4. Publish several events across different channels — notice each event gets a unique ID and timestamp.
  5. 5. Click Clear Log to delete the event log key from Redis.
6

Distributed Locking

String (SET NX EX)lock:<resource>

Multiple services may try to modify the same resource simultaneously, causing race conditions. Redis SET NX EX is atomic — only one caller gets OK. The lock value stores the owner ID so only the holder can release it, and the TTL guarantees automatic expiry if the holder crashes (deadlock prevention).

How it works
1. Worker calls SET lock:<resource> <workerId> EX <ttl> NX
2. Redis returns OK if key didn't exist (lock acquired)
   or (nil) if it did (lock held by another worker)
3. Only the lock holder can release it
4. TTL ensures auto-expiry if the holder crashes
Try it
  1. 1. Select 🔒 Distributed Locking.
  2. 2. Choose a Resource and Worker ID, set a Lock TTL.
  3. 3. Click Acquire Lock (SET NX EX) with worker-1 — the lock is acquired.
  4. 4. Switch to worker-2 and click Acquire Lock again — you'll see a red entry showing the lock is already held.
  5. 5. Switch back to worker-1 and click Release Lock (owner only) — only the holder can release it.
7

📦 Batch Write Buffer

List (RPUSH / LRANGE / DEL)batch-write:inventory-updates

High-frequency writes (stock updates, click events, sensor readings) can overwhelm a relational database if each event triggers an individual UPDATE. Redis acts as a write buffer — updates are appended to a List with RPUSH (microseconds), then flushed to the database in a single transaction. Multiple updates to the same row are aggregated before the flush, dramatically reducing DB writes.

How it works
1. Each stock update → RPUSH batch-write:inventory-updates <json>
2. Updates accumulate in Redis — DB not touched yet
3. Flush to DB: deltas for same product are aggregated
   → single SQLite transaction
4. Result: N queued updates → as few as 1 DB write per product
Try it
  1. 1. Select 📦 Batch Write Buffer.
  2. 2. Choose a Product and a Stock delta (e.g. -1 for a sale, +10 for a restock).
  3. 3. Click Queue Update (RPUSH) several times — each click appends a JSON entry to the Redis List.
  4. 4. Queue multiple updates for the same product — this is the key scenario.
  5. 5. Click Flush to DB (batch transaction) — all queued entries are read, deltas for the same product are summed, and a single UPDATE per product is executed.
  6. 6. Notice: if you queued 5 updates for the same product, the result panel shows 5 queued → 1 DB write.

Stopping Redis

When you're done with the tutorial, stop and remove the Redis container:

# Stop the container
docker stop redis-tutorial

# Remove the container (optional)
docker rm redis-tutorial

# To start it again later, re-run the docker run command from Step 1

Conclusion

This tutorial demonstrates that Redis is far more than a simple cache. It's a versatile in-memory data store that can power caching, session management, rate limiting, leaderboards, pub/sub messaging, distributed locking, and batch write buffering — all with sub-millisecond latency and atomic guarantees.

By working through the four tabs, you've seen how the same Redis instance can serve radically different use cases depending on which data structure and command set you choose. The key insight: pick the right Redis data structure for the job, and you get both performance and correctness for free.

Key Concepts Demonstrated

Cache-Aside vs. Write-Through

The two fundamental caching strategies — reactive lazy loading vs. proactive cache population — and when to use each.

Redis Data Structures

Hands-on use of Strings, Hashes, Lists, and Sorted Sets — each suited to different caching and data management patterns.

TTL & Cache Invalidation

Setting expiry on cache keys, observing automatic TTL-based expiry, and manually invalidating cache entries to force a fresh database read.

Atomic Operations

Using Redis atomic commands (INCR, SETNX, ZADD) for race-condition-free rate limiting, distributed locking, and leaderboard updates.

Pub/Sub Messaging

Broadcasting events to subscribers in real time using Redis Pub/Sub, combined with a List-based ring buffer for persistent event logging.

Production Patterns

Real-world patterns used in production systems: session management, API response caching, batch write buffering, and distributed locking with deadlock prevention.

Learn More

About the Author

Wayne Cheng is the founder and AI app developer at Audoir, LLC. Prior to founding Audoir, he worked as a hardware design engineer for Silicon Valley startups and an audio engineer for creative organizations. He holds an MSEE from UC Davis and a Music Technology degree from Foothill College.

Further Exploration

Explore the complete tutorial repository and experiment with extending the examples. Consider adding new caching strategies, connecting to a real external API, or exploring Redis Streams for event sourcing to deepen your understanding of Redis in production systems.

For more tutorials and AI-powered development tools, visit Audoir .