Henry Hoang

Caching with Redis

Imagine you are managing an e-commerce website. On the 11/11 super sale day, traffic spikes 100-fold. Millions of users...

Caching with Redis

Unit 5: Performance Patterns (Redis Caching) Topic Code: PERF-501 Reading Time: ~40 minutes


Learning Objectives

  • Explain the benefits of caching for system performance and scalability.
  • Introduce Redis and its core features as a key-value store.
  • Describe and differentiate caching strategies: Cache-Aside, Read-Through, and Write-Through.
  • Analyze common cache invalidation strategies (TTL, write-through, explicit invalidation).
  • Implement the Cache-Aside pattern in Python with Redis to optimize an API endpoint.

Section 1: Concept/Overview

1.1 Introduction

Imagine you are managing an e-commerce website. On the 11/11 super sale day, traffic spikes 100-fold. Millions of users simultaneously access the hottest product pages. For every access, your system has to run a complex SQL query to retrieve product information, check inventory, and fetch reviews from the database. The database quickly becomes overloaded, response times skyrocket, and finally, the website crashes. Customers are disappointed, and you lose revenue.

This is the problem that caching was born to solve. Instead of "bothering" the database for every identical request, we can store the result of the first access in a place with extremely fast retrieval speeds (the cache). Subsequent accesses will retrieve data directly from the cache without touching the database. This significantly reduces load on the database, increases application response speed, and improves the overall system's load-bearing capacity. In the modern IT industry, caching is no longer a "nice-to-have" option but has become an essential component in the architecture of most high-performance systems, from giant social networks like Facebook and Twitter to streaming platforms like Netflix.

1.2 Formal Definition

Caching is a performance optimization technique that involves storing copies of data in a temporary location (called a cache) so that future requests for that same data can be served faster. A cache is a high-speed data storage layer, typically located between the application layer and the original data storage layer (data source), such as a database.

The main goal of caching is to reduce latency and increase throughput.

  • Latency: The time required to retrieve data. Accessing data from RAM (where cache usually resides) is thousands of times faster than accessing it from a disk (where databases usually store data).
  • Throughput: The number of requests the system can handle per unit of time. By reducing the load on the database, the system can serve more users simultaneously.

Redis (REmote DIctionary Server) is an open-source, in-memory data structure store, used as a database, cache, and message broker. With the ability to read/write data with sub-millisecond latency, Redis is a top choice for implementing caching.

1.3 Analogy

Imagine your Database is a huge library containing millions of books. The Cache is the librarian's desk.

  • Without Cache: Every time a student (user) needs a popular book (hot data), they have to go deep into the bookshelves (database) to search. This process is very slow and laborious, especially when hundreds of students are looking for the same book. The library becomes congested.
  • With Cache: The librarian (application) notices that the book "Python Programming for Beginners" is very popular. After the first time going to find it, she decides to place a few copies right on her desk (cache). The next time another student asks to borrow this book, she just needs to grab it from the desk and give it to them immediately. This process is much faster, and the bookshelves inside the library are "quieter" to serve requests for less popular books.

In this example:

  • Library: Database (slow but contains everything).
  • Desk: Redis Cache (smaller, faster, only contains popular items).
  • Librarian: Your Application (decides what should be put on the desk).
  • Student: User/Client.

1.4 History

Redis was created by Salvatore Sanfilippo (also known by the nickname "antirez"), an Italian developer. The project started in early 2009. Antirez originally developed Redis to improve the scalability of his real-time web analytics startup. He was dissatisfied with the performance of traditional database systems (like MySQL) for this type of workload.

He needed a system that could handle a large number of write operations and serve data with extremely low latency. Instead of just being a simple key-value store, he designed Redis to support complex data structures like Lists, Sets, and Hashes from the start. This made Redis extremely flexible and powerful. Redis quickly became popular in the open-source community and is trusted by major companies like Twitter, GitHub, Pinterest, and Snapchat.


Section 2: Core Components

2.1 Architecture Overview

A typical caching architecture with Redis acts as an intermediary layer between the application and the main database.

+-----------+       1. Request data       +-----------------+       2. Cache Miss       +---------------+
|           | ------------------------> |                 | ----------------------> |               |
|  Client/  |                             |  Application    |                         |   Database    |
|  Service  |       5. Return data        |  (with Caching  |       3. Get data       |   (PostgreSQL,|
|           | <------------------------ |      Logic)     | <---------------------- |    MySQL)     |
+-----------+                             |                 |                         |               |
                                          +-------+---------+                         +---------------+
                                                  |   ^
                                                  |   | 4. Store in Cache
                                          2b. Cache Hit |   | (for next time)
                                                  |   v
                                          +-------+---------+
                                          |                 |
                                          |   Redis Cache   |
                                          |                 |
                                          +-----------------+

Workflow (Cache-Aside Pattern):

  1. Client sends a request to retrieve data (e.g., product info product:123).
  2. Application first checks if data product:123 exists in Redis Cache.
  • 2b. Cache Hit (Found): If yes, Redis returns data immediately to the Application. The Application returns it to the Client. The process ends here.
  • 2. Cache Miss (Not Found): If not, the Application proceeds to step 3.
  1. Application queries data from the main Database.
  2. Application receives data from the Database, then saves a copy of it to Redis Cache with a specific key (e.g., product:123) and usually an expiration time (TTL).
  3. Application returns data to the Client.

2.2 Key Components

In the context of caching, Redis provides the following core components:

Component 1: Key-Value Store

  • Definition: The foundation of Redis is a key-value store. Every piece of data in Redis is stored as a pair (key, value). key is a unique string used to identify data, and value can be a string, a number, or a more complex data structure.
  • Role: In caching, key is usually a unique identifier for a resource (e.g., user:100, product:_id_), and value is that resource's data (often in JSON format or a serialized string).
  • Syntax (using redis-cli):
# Set a value for a key
> SET user:101 '{"name": "Alice", "email": "alice@example.com"}'
OK

# Get a value by its key
> GET user:101
"{\"name\": \"Alice\", \"email\": \"alice@example.com\"}"

Component 2: Time-To-Live (TTL)

  • Definition: TTL is a feature that allows you to set a lifetime for a key. After this time expires, Redis will automatically delete that key.
  • Role: Extremely important in caching to ensure data does not become "stale". It also helps free up memory by automatically removing data that is rarely accessed or no longer valid. This is the simplest and most effective cache invalidation strategy.
  • Syntax:
# Set a key that will expire in 60 seconds
> SET session:xyz "some_session_data" EX 60
OK

# Check the remaining time to live for a key
> TTL session:xyz
(integer) 58

# After 60 seconds...
> GET session:xyz
(nil) # nil means the key does not exist

Component 3: Redis Data Structures

  • Definition: Unlike simple key-value stores that only allow string values, Redis supports many complex data structures.

  • Role: Allows for implementation of advanced caching patterns.

  • Strings: Most common for caching, storing HTML pages, JSON objects, etc.

  • Hashes: Ideal for caching objects with multiple fields. Instead of storing the whole JSON object, you can store it as a hash and only update/retrieve individual fields.

  • Lists: Used to cache ordered sequences of data, e.g., the 10 latest posts.

  • Sets: Used to cache collections of unique values, e.g., a list of tags for a post.

  • Syntax (Example with Hash):

# Store user 102's data as a hash
> HSET user:102 name "Bob" email "bob@example.com" age 30
(integer) 3

# Get just the name of user 102
> HGET user:102 name
"Bob"

# Get all fields of the user
> HGETALL user:102
1) "name"
2) "Bob"
3) "email"
4) "bob@example.com"
5) "age"
6) "30"

2.3 Comparison of Approaches (Caching Strategies)

ApproachProsConsWhen to use
Cache-Aside (Lazy Loading)- Cache logic is in the application, flexible.


- Resilient to Redis failure: If Redis goes down, the application can still work (just slower).



- Only caches data when it is actually requested, avoiding caching data that is never used. | - Cache Miss Penalty: The first request is always slower because of the 3 steps: query cache, query DB, write cache.



- Data in cache can become stale compared to DB until cache expires or is invalidated.



- Code is slightly more complex as the app manages both cache and DB. | Most common. Suitable for read-heavy systems where slight data latency is acceptable. Most general web applications. | | Read-Through | - Application logic is simpler: just talks to the cache.



- Cache manages retrieving data from DB upon cache miss. | - Requires a Redis provider or library that supports Read-Through.



- Initial access still has "cache miss penalty" similar to Cache-Aside.



- Harder to separate app logic and cache logic. | When you want data retrieval logic encapsulated and reusable. Often seen in systems using frameworks or platforms providing this mechanism (e.g., Hazelcast, Ehcache). | | Write-Through | - Data in cache and DB is always consistent.



- Application only needs to write to cache, cache handles writing to DB. |

  • Increased write latency because writing to 2 places.


- If Redis goes down, write operations might fail.



- Might cache data that is never read again, wasting memory. | Applications requiring very high data consistency between cache and DB, e.g., banking systems, session management. Not suitable for write-heavy systems. |


Section 3: Implementation

To practice, we will use Python with the redis-py library and Flask framework to build a simple API.

Setup:

# Create a virtual environment
python -m venv venv
source venv/bin/activate

# Install required libraries
pip install Flask redis

Level 1 - Basic (Beginner)

"Hello World" example to connect to Redis, perform SET and GET.

# basic_redis.py
import redis
import time

# Code Example #1: Connecting to Redis
try:
    # Connect to a local Redis instance
    # decode_responses=True will automatically decode responses from bytes to utf-8 strings
    r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)

    # Check if the connection is successful
    r.ping()
    print("Successfully connected to Redis!")

except redis.exceptions.ConnectionError as e:
    print(f"Could not connect to Redis: {e}")
    exit(1)

# Code Example #2: Basic SET, GET, and TTL
# A simple key-value pair
key = "greeting"
value = "Hello, Redis Fresher!"

print(f"\nSetting key '{key}' to '{value}'")
r.set(key, value)

# Retrieving the value
retrieved_value = r.get(key)
print(f"Retrieved value: {retrieved_value}")

# Setting a key with a Time-To-Live (TTL) of 5 seconds
ttl_key = "transient_key"
print(f"\nSetting key '{ttl_key}' with a 5-second TTL")
r.set(ttl_key, "This will disappear soon...", ex=5)

# Check TTL
print(f"TTL for '{ttl_key}': {r.ttl(ttl_key)} seconds")

# Wait for 6 seconds to see it expire
print("Waiting for 6 seconds...")
time.sleep(6)

# Try to get the expired key
expired_value = r.get(ttl_key)
print(f"Value of '{ttl_key}' after expiration: {expired_value}")

Running the script:

python basic_redis.py

Expected Output:

Successfully connected to Redis!

Setting key 'greeting' to 'Hello, Redis Fresher!'
Retrieved value: Hello, Redis Fresher!

Setting key 'transient_key' with a 5-second TTL
TTL for 'transient_key': 5 seconds
Waiting for 6 seconds...
Value of 'transient_key' after expiration: None

Common Errors:

  • Error 1: redis.exceptions.ConnectionError: Error 61 connecting to localhost:6379. Connection refused.
  • Description: This error occurs when the Python application cannot connect to the Redis server.
  • Fix: Ensure you have installed and started the Redis server. If using Docker, run docker run -d -p 6379:6379 redis. If installed directly, run the redis-server command.

Level 2 - Intermediate

Build an API endpoint to retrieve product information. Initially, it will be very slow. Then, we will apply the Cache-Aside pattern to optimize it.

# intermediate_api.py
from flask import Flask, jsonify
import redis
import time
import json

app = Flask(__name__)

# Connect to Redis
redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)

# Mock database of products
mock_db = {
    "product:101": {"name": "Laptop Pro", "price": 1200, "in_stock": True},
    "product:102": {"name": "Gaming Mouse", "price": 75, "in_stock": True},
    "product:103": {"name": "Mechanical Keyboard", "price": 150, "in_stock": False},
}

def get_product_from_db(product_id: str) -> dict:
    """A slow function to simulate a database call."""
    print(f"--- Database query for {product_id} ---")
    time.sleep(2)  # Simulate a 2-second delay for DB query
    return mock_db.get(product_id)

# Code Example #3: The slow, un-cached endpoint
@app.route('/products/slow/<product_id>')
def get_slow_product(product_id):
    """This endpoint fetches data directly from the 'database' every time."""
    start_time = time.time()

    product_data = get_product_from_db(f"product:{product_id}")

    end_time = time.time()
    duration = end_time - start_time

    if not product_data:
        return jsonify({"error": "Product not found"}), 404

    return jsonify({
        "data": product_data,
        "source": "database",
        "duration_seconds": duration
    })

# Code Example #4: Implementing Cache-Aside pattern
@app.route('/products/fast/<product_id>')
def get_fast_product(product_id):
    """This endpoint uses Redis caching to speed up responses."""
    start_time = time.time()

    cache_key = f"product:{product_id}"

    # 1. Check cache first
    cached_product = redis_client.get(cache_key)

    if cached_product:
        # Cache Hit!
        print(f"--- Cache HIT for {cache_key} ---")
        product_data = json.loads(cached_product) # Deserialize from JSON string
        source = "cache"
    else:
        # Cache Miss!
        print(f"--- Cache MISS for {cache_key} ---")
        # 2. If not in cache, get from DB
        product_data = get_product_from_db(cache_key)

        if product_data:
            # 3. Store in cache for next time with a 60-second TTL
            redis_client.set(cache_key, json.dumps(product_data), ex=60)
        source = "database"

    end_time = time.time()
    duration = end_time - start_time

    if not product_data:
        return jsonify({"error": "Product not found"}), 404

    return jsonify({
        "data": product_data,
        "source": source,
        "duration_seconds": duration
    })

if __name__ == '__main__':
    app.run(debug=True)

How to run and check:

  1. Run server: flask --app intermediate_api run
  2. Open browser or curl to call API:
  • Time 1 (Slow): curl http://127.0.0.1:5000/products/slow/101 -> Takes ~2 seconds.
  • Time 2 (Slow): curl http://127.0.0.1:5000/products/slow/101 -> Still takes ~2 seconds.
  • Time 1 (Fast - Cache Miss): curl http://127.0.0.1:5000/products/fast/101 -> Takes ~2 seconds, terminal prints --- Cache MISS ---.
  • Time 2 (Fast - Cache Hit): curl http://127.0.0.1:5000/products/fast/101 -> Response almost immediate (<0.01 seconds), terminal prints --- Cache HIT ---.

Level 3 - Advanced

The caching logic in get_fast_product can be reused but makes code hard to read. We will refactor it into a Python Decorator, a very common and professional pattern. We will also add cache invalidation logic.

# advanced_api.py
from flask import Flask, jsonify, request
import redis
import time
import json
from functools import wraps

app = Flask(__name__)
redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)

# Mock DB (same as before)
mock_db = {
    "product:101": {"name": "Laptop Pro", "price": 1200, "in_stock": True},
    "product:102": {"name": "Gaming Mouse", "price": 75, "in_stock": True},
}

def get_product_from_db(product_id: str) -> dict:
    print(f"--- Database query for {product_id} ---")
    time.sleep(2)
    return mock_db.get(product_id)

# Code Example #5: The Caching Decorator
def cache(ttl: int):
    """
    A decorator to cache the result of a function with a given TTL.
    It assumes the first argument of the decorated function is the cache key identifier.
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # Generate a cache key from function name and its arguments
            # This is more robust than assuming the first arg is the key
            key_parts = [func.__name__] + list(map(str, args)) + [f"{k}={v}" for k, v in sorted(kwargs.items())]
            cache_key = ":".join(key_parts)

            cached_result = redis_client.get(cache_key)
            if cached_result:
                print(f"--- Cache HIT for {cache_key} ---")
                return json.loads(cached_result)

            print(f"--- Cache MISS for {cache_key} ---")
            result = func(*args, **kwargs)
            if result is not None:
                redis_client.set(cache_key, json.dumps(result), ex=ttl)
            return result
        return wrapper
    return decorator

# Code Example #6: Applying the decorator to a clean function
@app.route('/products/v3/<product_id>')
@cache(ttl=60)
def get_product_v3(product_id):
    """
    The business logic is now clean. The caching is handled by the decorator.
    The decorator generates a key like: 'get_product_v3:101'
    """
    product_data = get_product_from_db(f"product:{product_id}")
    return product_data # We just return the data, decorator handles caching.


# Code Example #7: Implementing explicit cache invalidation
@app.route('/products/v3/<product_id>', methods=['PUT'])
def update_product(product_id):
    """
    When a product is updated, we MUST invalidate its cache to avoid serving stale data.
    """
    # Simulate updating the product in the database
    update_data = request.get_json()
    db_key = f"product:{product_id}"
    if db_key not in mock_db:
        return jsonify({"error": "Product not found"}), 404

    mock_db[db_key].update(update_data)
    print(f"--- Updated product {db_key} in DB: {mock_db[db_key]} ---")

    # Explicitly delete the cache key associated with the decorated function
    # The key name must match the one generated by the decorator
    cache_key_to_invalidate = f"get_product_v3:{product_id}"
    deleted_count = redis_client.delete(cache_key_to_invalidate)

    if deleted_count > 0:
        print(f"--- Invalidated cache for key: {cache_key_to_invalidate} ---")

    return jsonify({"status": "updated", "invalidated_cache": deleted_count > 0})


if __name__ == '__main__':
    app.run(port=5001, debug=True) # Run on a different port to avoid conflict

How to run and check:

  1. Run server: flask --app advanced_api run --port 5001
  2. GET Time 1 (Miss): curl http://127.0.0.1:5001/products/v3/101 -> Takes ~2 seconds.
  3. GET Time 2 (Hit): curl http://127.0.0.1:5001/products/v3/101 -> Extremely fast.
  4. Update price (Invalidation): curl -X PUT -H "Content-Type: application/json" -d '{"price": 1250}' http://127.0.0.1:5001/products/v3/101 -> Server terminal will print --- Invalidated cache... ---.
  5. GET Time 3 (Miss again): curl http://127.0.0.1:5001/products/v3/101 -> Takes ~2 seconds again, because cache was deleted and must retrieve latest data from DB (with price 1250).

Section 4: Best Practices

PracticeWhyExample
Always Set TTLAvoid wasting memory and prevent stale data from persisting forever in cache. This is the most important defense mechanism.redis_client.set("mykey", "value", ex=3600) (expires after 1 hour)
Use Key Naming ConventionHelps manage, debug, and avoid key collisions. A good convention is object-type:id:field, e.g., user:101:profile.user:101, product:_id_, session:xyz
Serialize DataRedis stores values as byte strings. Always serialize complex objects (like Python dictionaries) to a standard format like JSON before storing.redis_client.set("user:101", json.dumps(user_dict))
Handle Connection ErrorsNetwork is unstable or Redis server might crash. Your application should have a fallback mechanism (e.g., read directly from DB) instead of crashing completely.try...except redis.exceptions.ConnectionError:
Invalidate Cache on Data ChangeWhen data in DB is updated (UPDATE, DELETE), delete (invalidate) the corresponding key in cache to ensure consistency.redis_client.delete("product:101")

❌ DON'Ts - Avoid

Anti-patternConsequenceHow to avoid
Caching Large, Rarely Accessed DataWastes valuable Redis RAM. Cache should only be for "hot data" - frequently accessed data.Analyze and only cache endpoints/data with high traffic.
Using Redis as Primary DB without PersistenceRedis is in-memory by default. If server restarts, all data is lost.If durability is needed, enable AOF (Append Only File) or RDB (Redis Database Backup). But as a cache, data loss is usually acceptable.
Using Expensive Commands in ProductionCommands like KEYS *, FLUSHALL, FLUSHDB can block Redis server for a long time when there are many keys, affecting all other clients.Use SCAN instead of KEYS. Minimize use of FLUSHALL in production.
No Strategy for Cache StampedeWhen a hot key expires, thousands of requests will simultaneously "miss" and attack the DB to retrieve data, causing DB crash.Use techniques like "locking" (only one request allowed to populate cache) or "probabilistic early expiration".

🔒 Security Considerations

  • Set Password: Always configure requirepass in redis.conf to require client authentication.
# redis.conf
requirepass your_strong_password
  • Bind IP Address: Only allow connections from trusted IP addresses (e.g., application server) by configuring bind 127.0.0.1 your_app_server_ip.
  • Rename or Disable Dangerous Commands: You can rename commands like FLUSHALL to a hard-to-guess string to avoid accidental execution or attacks. rename-command FLUSHALL "".
  • Run Redis with Low Privilege User: Do not run Redis as root.

⚡ Performance Tips

  • Pipelining: Reduce round-trip time (RTT) overhead by sending multiple commands at once and receiving all responses in one go.
# Code Example #8: Using pipeline for batch operations
pipe = redis_client.pipeline()
pipe.set('user:101:visits', 10)
pipe.incr('user_logins')
pipe.get('user:101:visits')
# The commands are sent to Redis all at once
results = pipe.execute()
# results will be [True, 1, '10']
  • Use MGET/MSET: When retrieving or setting multiple keys, use MGET (multi-get) or MSET (multi-set) instead of calling GET/SET in a loop. This significantly reduces round-trips.
  • Choose Right Data Structure: Use Hashes to store objects instead of JSON strings if you frequently only need to access a few fields of the object. This saves bandwidth and deserialization costs.

Section 5: Case Study

5.1 Scenario

Company/Project: "TicketNow", an online concert ticket platform. Requirements: The system must handle extremely high traffic in the first few minutes when tickets go on sale for a famous artist's show. The endpoint /events/{event_id} displaying event details (name, date, venue) must have a response time under 50ms, even with 10,000 requests/second. Constraints: Database is PostgreSQL, overloading every time tickets go on sale. Event details rarely change.

5.2 Problem Analysis

Log analysis shows that 99% of requests to endpoint /events/{event_id} are read operations (GET). Each request executes a complex SQL JOIN query to aggregate information from multiple tables (events, venues, artists). This creates a massive read load on the database, causing locks, increased latency, and eventually timeouts, preventing users from viewing event info to buy tickets.

5.3 Solution Design

We will deploy the Cache-Aside strategy using Redis.

  1. Architecture: Place a Redis instance as a cache layer between the web server and PostgreSQL database.
  2. Data Flow:
  • When there is a request to /events/{event_id}, the application creates a cache key, e.g., event_details:{event_id}.
  • Application checks this key in Redis.
  • Cache Hit: If present, return JSON data from Redis immediately.
  • Cache Miss: If not, application queries database, serializes result to JSON, saves to Redis with long TTL (e.g., 1 hour, since event info changes rarely), and then returns to user.
  1. Invalidation: When admin updates event info (e.g., changes time), system sends a DELETE command to Redis to remove corresponding event_details:{event_id} key, ensuring next access gets latest data.

5.4 Implementation

# ticketnow_solution.py
from flask import Flask, jsonify
import redis
import json
import time

# --- Setup (similar to previous examples) ---
app = Flask(__name__)
redis_client = redis.Redis(host='localhost', port=6379, db=1, decode_responses=True)

# Mock PostgreSQL Database
def get_event_details_from_db(event_id: int) -> dict:
    """Simulates a slow, complex SQL JOIN query."""
    print(f"!!! POSTGRESQL QUERY for event {event_id} !!!")
    time.sleep(1.5) # Simulate 1.5s query time

    # In reality, this data comes from a complex JOIN
    events = {
        888: {"event_name": "The Grand Finale Tour", "artist": "The Rockers", "venue": "City Stadium"},
        999: {"event_name": "Acoustic Evening", "artist": "Solo Singer", "venue": "Royal Theatre"}
    }
    return events.get(event_id)

# --- Solution Implementation ---
@app.route('/events/<int:event_id>')
def get_event_details(event_id):
    """
    Endpoint to get event details, optimized with Cache-Aside pattern.
    """
    cache_key = f"event_details:{event_id}"

    # 1. Check cache
    try:
        cached_event = redis_client.get(cache_key)
        if cached_event:
            print(f"--- CACHE HIT for {cache_key} ---")
            return jsonify(json.loads(cached_event))
    except redis.exceptions.RedisError as e:
        # If Redis is down, we can log the error and proceed to the database.
        # This makes the system more resilient.
        print(f"Redis error: {e}. Falling back to database.")


    # 2. Cache Miss: Get from DB
    print(f"--- CACHE MISS for {cache_key} ---")
    event_data = get_event_details_from_db(event_id)

    if not event_data:
        return jsonify({"error": "Event not found"}), 404

    # 3. Store in cache with a 1-hour TTL
    try:
        # We add the source to the data for demonstration purposes
        event_data_with_source = {**event_data, "source": "database"}
        redis_client.set(cache_key, json.dumps(event_data_with_source), ex=3600)
    except redis.exceptions.RedisError as e:
        print(f"Could not write to Redis cache: {e}")

    return jsonify(event_data_with_source)

5.5 Results & Lessons Learned

  • Improved Metrics:

  • Latency: Average response time for repeat requests dropped from ~1500ms to < 10ms (99% reduction).

  • Database Load: Number of read queries on events table decreased by over 95% during peak hours.

  • Throughput: System can handle from 500 requests/second up to over 10,000 requests/second without upgrading database.

  • Availability: System became more stable, no longer crashing due to database overload.

  • Lessons Learned:

  1. Identify the Bottleneck: Caching is most effective when applied in the right place. Identifying slow, read-heavy queries is the first important step.
  2. TTL is Your Friend: Setting reasonable TTL is the simplest and most effective way to balance performance and data freshness.
  3. Resilience Matters: Design the system so that cache failure doesn't crash the entire application. Always have a fallback mechanism to the main data source.
  4. Simple is Effective: For read-heavy workloads, the Cache-Aside strategy is a powerful solution, relatively easy to implement, and yields enormous benefits.

References

On this page