Speed Up a Python API with Redis
Cache GitHub API responses in Redis and measure the speed difference.
Introduction
30 Second Summary
Every time you use a fast app, there is a good chance caching is doing the heavy lifting behind the scenes. Instead of fetching the same data over and over, a cache stores it close by so the next request is almost instant.
In this project, you will build a Python API that fetches data from GitHub and caches responses in Redis. You will measure the speed difference between cached and uncached requests to see exactly how much time caching saves.
What You'll Build
A Flask API server that fetches GitHub user profiles and caches the responses in Redis, proving that cached requests return in under 1 millisecond compared to hundreds of milliseconds without caching.
By the end of this project, you'll have:
- A Flask web server with API endpoints that fetch GitHub user data and return it as JSON.
- A Redis caching layer that stores responses with a configurable TTL (time-to-live) and prints whether each request was a cache hit or miss.
- A cache invalidation endpoint that lets you force a fresh data fetch on demand without waiting for the cache to expire.
- Secret Mission: A real-time cache statistics dashboard that tracks total hits, misses, and hit rate percentage.
Are there any prerequisites?
None. This project includes full setup instructions for every tool you need, including Python, VS Code, WSL2, and Redis. You do not need any prior experience with caching or web development.
Set Up Python, VS Code, and WSL2
Before you can build a caching API, you need three tools working together on your machine. VS Code is your code editor. Python runs your Flask server. Redis stores cached data so your API can skip expensive network calls.
Redis does not run natively on Windows. The official solution is WSL2 (Windows Subsystem for Linux), which lets you run a real Linux environment inside Windows. You will install Redis inside that Linux environment and connect to it from your Python code running on Windows.
In this step, get ready to:
- Install VS Code and Python on Windows.
- Enable WSL2 and install Ubuntu.
- Install and start Redis inside WSL2.
Install VS Code and Python
You need a code editor and a Python runtime before you can write or run any code.
- Download VS Code from https://code.visualstudio.com.
- Run the installer and accept the default settings.
- Download Python from https://www.python.org.
- Run the Python installer. On the very first screen, check the box that says Add python.exe to PATH before clicking Install Now.
Why does PATH matter?
Adding Python to PATH lets you run python from any terminal window. Without it, your computer will not know where to find the Python program when you type the command.
Now open VS Code so you can verify the installation.
- Press the Windows key to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Open the built-in terminal by pressing Ctrl+` (the backtick key, above Tab).
- Verify Python is installed by running this command:
python --version
✔️ I see a version number
You should see something like Python 3.x.x printed in the terminal. Python is ready to go.
ⓧ 'python' is not recognized
This means Python was not added to your PATH.
- Re-run the Python installer from https://www.python.org.
- Select Modify and make sure Add python.exe to PATH is checked.
- Close and reopen VS Code, then run python --version again.
Enable WSL2 and install Ubuntu
WSL2 gives you a full Linux environment running inside Windows. You need this because Redis is designed to run on Linux.
What is WSL2?
WSL2 stands for Windows Subsystem for Linux version 2. It runs a real Linux kernel alongside Windows, letting you use Linux tools without dual-booting or setting up a virtual machine.
Your system needs Windows 10 version 2004 or higher, or Windows 11.
- Press the Windows key, type PowerShell, then right-click Windows PowerShell and select Run as administrator.
- Install WSL2 with Ubuntu by running this command:
wsl --install
What does this command do?
This single command enables WSL2, downloads the Linux kernel, and installs Ubuntu as your default Linux distribution. It handles everything in one step.
Your computer will need to restart
After the command finishes, you will be prompted to restart your computer. Save any open work, then restart when asked.
- Restart your computer when prompted.
- After restart, an Ubuntu terminal window will open automatically to finish setup.
- Create a username and password when asked. This is your Linux account inside WSL2.
Remember your Linux password
You will need this password later when running commands with sudo. The password will not show characters as you type. That is normal.
WSL install not working?
Make sure you are running PowerShell as Administrator (right-click, Run as administrator). If you see a message about enabling a Windows feature, your Windows version may be too old. You need Windows 10 version 2004 or higher, or Windows 11.
help me troubleshoot my WSL2 installation
Install Redis inside WSL2
Now that Ubuntu is running inside WSL2, you can install Redis from the official Redis package repository. This gives you the latest stable version.
- Press the Windows key, type Ubuntu, and press Enter to open your Ubuntu terminal.
- Add the official Redis package repository and install Redis by running these commands one at a time:
curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list
sudo apt-get update
sudo apt-get install redis
What do these commands do?
- The first two commands add the official Redis package repository to Ubuntu so it knows where to download Redis from.
- sudo apt-get update refreshes the list of available packages.
- sudo apt-get install redis downloads and installs Redis.
You will be prompted to enter your Linux password for the sudo commands. If asked to confirm the install with Y/n, type Y and press Enter.
- Start the Redis server by running:
sudo service redis-server start
- Verify Redis is running by running:
redis-cli ping
✔️ I see PONG
Redis is running and responding to commands. You are all set.
ⓧ Could not connect to Redis
If you see a connection refused error, Redis did not start properly.
- Try starting it again with:
sudo service redis-server restart
- Then run redis-cli ping again.
- If it still fails, make sure you are inside the Ubuntu terminal (not PowerShell).
help me get Redis running in WSL2
What is Redis?
Redis is an in-memory key-value store. It keeps data in RAM instead of on disk, which makes reads and writes extremely fast. Production systems use it to cache API responses, session data, and frequently accessed queries.
Your development environment is ready. Next up, you will build a Flask API that fetches data from GitHub and measure how long each request takes.
Create the Flask API Without Caching
Your development environment is ready with Python, VS Code, and Redis running inside WSL2.
Now you need an API to cache. In this step, you will build a Flask web server that fetches user profiles from the GitHub API. Every request will call GitHub directly, which means every response takes hundreds of milliseconds. This slow baseline is exactly what you will speed up with caching in the next step.
In this step, get ready to:
- Create a project folder with a Python virtual environment and install dependencies.
- Build a Flask endpoint that fetches GitHub user data and prints response times.
- Test the API and observe how long each uncached request takes.
Create a project folder and virtual environment
A virtual environment keeps your project's packages isolated from other Python projects on your machine. You will create one now and install the three libraries this project needs.
- Open VS Code. Press the Windows key, type VS Code, and press Enter.
- Open the terminal inside VS Code by pressing Ctrl+` (backtick key, next to the 1 key).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir redis-cache-project
cd redis-cache-project
- Confirm the folder was created by running:
ls
You should see an empty directory listing since the folder is brand new.
- Create a Python virtual environment by running:
python -m venv venv
- Activate the virtual environment by running:
venv\Scripts\activate
Your terminal prompt should now show (venv) at the beginning. This confirms the virtual environment is active.
Seeing a script execution error?
PowerShell blocks scripts by default. Fix this by running the following command, then try activating again:
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
help me fix the PowerShell execution policy error when activating my Python virtual environment
- Install the project dependencies by running:
pip install flask redis requests
You should see output confirming that flask, redis, and requests were successfully installed along with their dependencies.
What are these three packages?
- flask is a lightweight web framework for building APIs in Python.
- redis (redis-py) is the Python client for talking to a Redis server. You will use it in the next step.
- requests makes it easy to call external APIs like GitHub's.
pip install failed?
Make sure you see (venv) in your terminal prompt. If not, run the activate command again. If you see a permissions error, confirm you are inside the virtual environment and not trying to install globally.
help me fix a pip install error in my Python virtual environment on Windows
Build a basic Flask app that fetches GitHub user data
Your API will have one endpoint that accepts a GitHub username, fetches that user's public profile from the GitHub API, and returns a simplified JSON response. You will also add timing code so you can measure exactly how long each request takes.
- Create a new file called app.py inside your redis-cache-project folder. In VS Code, click File in the top menu, then New File.
- Save it immediately as app.py by pressing Ctrl+S and navigating to your redis-cache-project folder on the Desktop.
- Add the imports and Flask app setup by pasting this code into app.py:
import time
import json
import requests
from flask import Flask, jsonify
app = Flask(__name__)
What does this code do?
- time lets you measure how long each API request takes in milliseconds.
- json handles converting data to and from JSON strings. You will use this when you add caching later.
- requests makes HTTP calls to external APIs like GitHub's.
- Flask(__name__) creates your web application instance.
- Below the setup code, add the route function and the app entry point:
@app.route("/github/<username>")
def get_github_user(username):
start_time = time.time()
# Call the GitHub API for this user's profile
response = requests.get(f"https://api.github.com/users/{username}")
if response.status_code != 200:
return jsonify({"error": "User not found"}), 404
# Extract the fields we care about
user_data = response.json()
result = {
"username": user_data["login"],
"name": user_data["name"],
"public_repos": user_data["public_repos"],
"followers": user_data["followers"],
"bio": user_data["bio"],
}
# Print how long the request took
elapsed = (time.time() - start_time) * 1000
print(f"Fetched from GitHub for {username} - {elapsed:.2f}ms")
return jsonify(result)
if __name__ == "__main__":
app.run(debug=True)
What does this code do?
- @app.route("/github/<username>") creates an endpoint that accepts any GitHub username as part of the URL.
- start_time = time.time() records the exact moment the request begins so you can calculate elapsed time later.
- requests.get(...) calls the GitHub API. This is the slow network call you will eventually cache.
- The result dictionary extracts only the five fields you care about from GitHub's large response.
- The print() line logs the request duration in milliseconds so you can compare cached vs uncached speeds later.
- Save app.py by pressing Ctrl+S.
Seeing red underlines in VS Code?
VS Code might not recognize the packages if it is not using your virtual environment's Python interpreter. Press Ctrl+Shift+P, type Python: Select Interpreter, and choose the one inside your venv folder.
help me fix red underlines for flask and requests imports in VS Code
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete app.py file at this point:
import time
import json
import requests
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/github/<username>")
def get_github_user(username):
start_time = time.time()
response = requests.get(f"https://api.github.com/users/{username}")
if response.status_code != 200:
return jsonify({"error": "User not found"}), 404
user_data = response.json()
result = {
"username": user_data["login"],
"name": user_data["name"],
"public_repos": user_data["public_repos"],
"followers": user_data["followers"],
"bio": user_data["bio"],
}
elapsed = (time.time() - start_time) * 1000
print(f"Fetched from GitHub for {username} - {elapsed:.2f}ms")
return jsonify(result)
if __name__ == "__main__":
app.run(debug=True)
Test the uncached API
Time to see your API in action. You will run the Flask server and make requests to measure how long each one takes without any caching.
- Start the Flask development server by running this command in your VS Code terminal:
python app.py
You should see output indicating Flask is running on http://localhost:5000.
Seeing a ModuleNotFoundError?
This means your virtual environment is not active. Check that your terminal prompt shows (venv) at the beginning. If not, run venv\Scripts\activate again and then retry python app.py.
help me fix ModuleNotFoundError when running my Flask app
- Open your web browser and navigate to:
http://localhost:5000/github/octocat
Your browser should display JSON data about the GitHub user octocat, including their name, bio, follower count, and number of public repos.
- Switch back to VS Code and look at the terminal output.
- Refresh the browser page 2-3 more times and watch the terminal after each refresh.
Each request prints a line showing how long it took. Every single request takes hundreds of milliseconds because the app calls GitHub every time.
Why is every request slow?
Right now, your API has no memory. Every time someone asks for the same username, it makes a brand new network call to GitHub. That round trip across the internet is what takes hundreds of milliseconds.
In the next step, you will add Redis caching so the first request fetches from GitHub, but every repeat request returns instantly from the local cache.
GitHub API rate limit
The GitHub API allows 60 requests per hour without authentication. This is more than enough for testing, but avoid refreshing dozens of times in a row.
- Stop the Flask server by pressing Ctrl+C in the terminal.
You now have proof that every request is slow. Next up, you will add Redis caching to make repeat requests return in under 1 millisecond.
Add Redis Caching
In the previous step, you built a working API that fetches GitHub user data. Every request goes out to the GitHub API and comes back in hundreds of milliseconds.
That's fine for a few requests, but production APIs handle thousands per second. Hitting an external service every time is slow and wasteful when the data rarely changes. In this step, you'll wire up Redis as a caching layer so repeat requests are served instantly from memory.
In this step, get ready to:
- Connect your Python app to the Redis server running in WSL2.
- Implement the cache-aside pattern in your API endpoint.
- Measure the performance difference between cached and uncached responses.
Connect to Redis from Python
Before your app can cache anything, it needs a connection to the Redis server you installed in WSL2. You'll also define a constant for how long cached data should live.
- If your Flask app is still running in the terminal, press Ctrl+C to stop it.
- In app.py, find the import lines at the top of the file. Add the Redis import by pasting this line after import requests:
import redis
- Now find the line app = Flask(__name__). Paste the following code below it:
# Connect to Redis running in WSL2
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
CACHE_TTL = 60 # Cache expires after 60 seconds
What does this code do?
r = redis.Redis(...) creates a connection to the Redis server on port 6379 (Redis's default port). The decode_responses=True setting tells redis-py to return normal strings instead of raw bytes.
CACHE_TTL = 60 sets the time-to-live for cached data to 60 seconds. After 60 seconds, Redis automatically deletes the cached entry so your app fetches fresh data on the next request.
What is TTL?
TTL stands for time-to-live. It's a countdown timer attached to a cached value. When the timer hits zero, Redis deletes the value automatically. This prevents your cache from serving stale data forever.
- Save app.py.
Seeing a connection error when you run later?
If Redis isn't running in WSL2, your app will fail to connect. Open your Ubuntu terminal and start Redis with redis-server --daemonize yes. Verify it's running by typing redis-cli ping. You should see PONG.
Help me debug a Redis connection error in my Flask app
Implement the cache-aside pattern
The cache-aside pattern is the most common caching strategy in production APIs. Your app checks the cache first. If the data is there (a cache hit), return it immediately. If not (a cache miss), fetch from the source, store it in the cache for next time, then return the response.
- In app.py, find your existing get_github_user function. It currently looks like this:
@app.route("/github/<username>")
def get_github_user(username):
start_time = time.time()
response = requests.get(f"https://api.github.com/users/{username}")
if response.status_code != 200:
return jsonify({"error": "User not found"}), 404
user_data = response.json()
result = {
"username": user_data["login"],
"name": user_data["name"],
"public_repos": user_data["public_repos"],
"followers": user_data["followers"],
"bio": user_data["bio"],
}
elapsed = (time.time() - start_time) * 1000
print(f"Fetched from GitHub for {username} - {elapsed:.2f}ms")
return jsonify(result)
- Replace the entire function (from @app.route down to return jsonify(result)) with the following code:
@app.route("/github/<username>")
def get_github_user(username):
cache_key = f"github:{username}"
start_time = time.time()
# Check cache first (cache-aside pattern)
cached_data = r.get(cache_key)
if cached_data:
# Cache hit
elapsed = (time.time() - start_time) * 1000
print(f"CACHE HIT for {username} - {elapsed:.2f}ms")
return jsonify(json.loads(cached_data))
# Cache miss - fetch from GitHub
response = requests.get(f"https://api.github.com/users/{username}")
if response.status_code != 200:
return jsonify({"error": "User not found"}), 404
user_data = response.json()
result = {
"username": user_data["login"],
"name": user_data["name"],
"public_repos": user_data["public_repos"],
"followers": user_data["followers"],
"bio": user_data["bio"],
}
# Store in cache with TTL
r.setex(cache_key, CACHE_TTL, json.dumps(result))
elapsed = (time.time() - start_time) * 1000
print(f"CACHE MISS for {username} - {elapsed:.2f}ms")
return jsonify(result)
What does this code do?
- cache_key = f"github:{username}" builds a Redis key using a colon-separated namespace. For the user octocat, the key becomes github:octocat.
- r.get(cache_key) asks Redis if it already has data for this key. If yes, the function returns it immediately without calling GitHub.
- r.setex(cache_key, CACHE_TTL, json.dumps(result)) stores the GitHub response in Redis with a 60-second expiration. setex means "set with expiration."
- json.dumps(result) converts the Python dictionary to a JSON string for storage. json.loads(cached_data) converts it back to a dictionary when reading from the cache.
Why store JSON strings in Redis?
Redis is a key-value store that holds string values. Python dictionaries can't be stored directly, so you serialize them to JSON strings with json.dumps() before writing and deserialize with json.loads() when reading.
- Save app.py.
Seeing a syntax error?
Make sure you replaced the entire old function including the @app.route decorator line. Having two @app.route("/github/<username>") definitions will cause errors. Check there's only one copy of the function in your file.
Help me fix a syntax error in my cache-aside code
Test cached vs uncached performance
Now you'll see the caching in action. The first request will be a cache miss (slow). The second identical request will be a cache hit (fast).
- Start your Flask app by running this command:
python app.py
- Open your browser and navigate to:
http://localhost:5000/github/octocat
- Look at your terminal. You should see a line like:
CACHE MISS for octocat - 312.45ms
This is a cache miss. Redis didn't have the data, so your app went to GitHub to fetch it. The response took hundreds of milliseconds.
- Now refresh the same page in your browser (press F5 or Cmd+R on macOS).
- Check your terminal again. You should see:
CACHE HIT for octocat - 0.42ms
This is a cache hit. Redis already had the data stored, so your app returned it in under 1 millisecond. No call to GitHub needed.
Why is the difference so dramatic?
A cache miss requires a network round-trip to GitHub's servers (hundreds of milliseconds). A cache hit reads from Redis running on your own machine (sub-millisecond). This is the core value of caching. You trade a small amount of memory for a massive speed improvement on repeat requests.
Still seeing slow responses on the second request?
Make sure Redis is running. Open your Ubuntu terminal and run redis-cli ping. If you don't see PONG, start Redis with redis-server --daemonize yes.
Also check that you saved app.py and restarted the server after making your changes. Unsaved edits won't take effect.
Help me figure out why my cached responses are still slow
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here's the complete app.py at this point:
import time
import json
import requests
import redis
from flask import Flask, jsonify
app = Flask(__name__)
# Connect to Redis running in WSL2
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
CACHE_TTL = 60 # Cache expires after 60 seconds
@app.route("/github/<username>")
def get_github_user(username):
cache_key = f"github:{username}"
start_time = time.time()
# Check cache first (cache-aside pattern)
cached_data = r.get(cache_key)
if cached_data:
# Cache hit
elapsed = (time.time() - start_time) * 1000
print(f"CACHE HIT for {username} - {elapsed:.2f}ms")
return jsonify(json.loads(cached_data))
# Cache miss - fetch from GitHub
response = requests.get(f"https://api.github.com/users/{username}")
if response.status_code != 200:
return jsonify({"error": "User not found"}), 404
user_data = response.json()
result = {
"username": user_data["login"],
"name": user_data["name"],
"public_repos": user_data["public_repos"],
"followers": user_data["followers"],
"bio": user_data["bio"],
}
# Store in cache with TTL
r.setex(cache_key, CACHE_TTL, json.dumps(result))
elapsed = (time.time() - start_time) * 1000
print(f"CACHE MISS for {username} - {elapsed:.2f}ms")
return jsonify(result)
if __name__ == "__main__":
app.run(debug=True)
Your API now serves repeat requests in under a millisecond. Next up, you'll explore what happens when the cache expires and add a way to manually clear it.
Explore TTL and Cache Invalidation
You've proven that caching speeds up repeated requests. Now it's time to understand what happens when cached data expires.
Every cached entry has a limited lifetime controlled by its TTL (time-to-live). Once the TTL countdown reaches zero, Redis automatically deletes the key. In this step, you'll observe that expiration in real time and build an endpoint that lets you manually invalidate cache entries on demand.
In this step, get ready to:
- Observe TTL countdown and cache expiration in real time.
- Add a manual cache invalidation endpoint to your API.
- Test the complete caching lifecycle from miss to hit to invalidation.
Observe TTL expiration
Your Flask app is still running from the previous step. Each cached entry lives for 60 seconds (the CACHE_TTL you set earlier). You can watch this countdown in real time using the Redis CLI.
- In your browser, visit http://localhost:5000/github/octocat to create a fresh cache entry.
- Open your Ubuntu terminal from the Start menu (the same one where Redis is running).
- Check how many seconds remain on the cached key by running this command:
redis-cli TTL github:octocat
What does this command do?
redis-cli TTL github:octocat asks Redis how many seconds remain before the github:octocat key expires. You'll see a number counting down from 60.
If you see -2, the key has already expired (you waited too long). If you see -1, the key exists but has no expiration set.
- Run the same redis-cli TTL github:octocat command a few more times over the next 30 seconds. You'll see the number decrease each time.
- Wait until 60 seconds have passed since your browser request, then run the TTL command again. You should see -2, which means the key no longer exists.
- Go back to your browser and refresh http://localhost:5000/github/octocat. Check your VS Code terminal.
You should see CACHE MISS printed in the terminal. The cache expired, so your app fetched fresh data from GitHub.
Still seeing CACHE HIT?
You may not have waited the full 60 seconds. Run redis-cli TTL github:octocat in your Ubuntu terminal to check. If the number is still positive, the cache hasn't expired yet.
help me understand why my Redis cache isn't expiring
Add a manual cache invalidation endpoint
Waiting 60 seconds every time you want fresh data is impractical. In production, you often need to force a cache refresh immediately. A cache invalidation endpoint lets you delete a specific cached entry on demand.
- Stop your Flask app by pressing Ctrl+C in the VS Code terminal.
- In app.py, find the cache hit block. Locate this line inside the if cached_data: section:
elapsed = (time.time() - start_time) * 1000
print(f"CACHE HIT for {username} - {elapsed:.2f}ms")
- Add two lines between elapsed and the print statement, so the block looks like this:
elapsed = (time.time() - start_time) * 1000
r.hincrby("cache:stats", "hits", 1)
r.hincrby("cache:stats", "requests", 1)
print(f"CACHE HIT for {username} - {elapsed:.2f}ms")
- Now find the cache miss block further down. Locate this line near the bottom of the function:
elapsed = (time.time() - start_time) * 1000
print(f"CACHE MISS for {username} - {elapsed:.2f}ms")
- Add two tracking lines between elapsed and the print statement, so it looks like this:
elapsed = (time.time() - start_time) * 1000
r.hincrby("cache:stats", "misses", 1)
r.hincrby("cache:stats", "requests", 1)
print(f"CACHE MISS for {username} - {elapsed:.2f}ms")
What does hincrby do?
r.hincrby("cache:stats", "hits", 1) atomically increments a field inside a Redis hash. Think of a hash as a mini dictionary stored under a single key.
The key cache:stats holds fields like hits, misses, and requests. Each call increments one field by 1. These counters will be useful later.
- Now add the cache invalidation endpoint. Scroll to the bottom of app.py and paste this new function above the if __name__ == "__main__": line:
@app.route("/github/<username>/cache", methods=["DELETE"])
def invalidate_cache(username):
cache_key = f"github:{username}"
deleted = r.delete(cache_key)
if deleted:
return jsonify({"message": f"Cache cleared for {username}"})
return jsonify({"message": f"No cache entry found for {username}"})
What does this code do?
This creates a DELETE endpoint at /github/<username>/cache. When called, it uses r.delete(cache_key) to remove the cached entry from Redis immediately.
The r.delete() call returns the number of keys deleted (1 if found, 0 if not). The endpoint uses this to return an appropriate message.
- Save app.py.
- Restart your Flask app by running:
python app.py
You should see Flask start up with no errors and display the development server URL.
Seeing a SyntaxError or indentation error?
Make sure the hincrby lines inside the if cached_data: block use 8 spaces of indentation (two levels). The lines in the cache miss section use 4 spaces (one level). Also make sure the new invalidate_cache function is placed above if __name__ == "__main__": with a blank line separating them.
help me fix my Python indentation error
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here's what your complete app.py should look like at this point:
import time
import json
import requests
import redis
from flask import Flask, jsonify
app = Flask(__name__)
# Connect to Redis running in WSL2
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
CACHE_TTL = 60 # Cache expires after 60 seconds
@app.route("/github/<username>")
def get_github_user(username):
cache_key = f"github:{username}"
start_time = time.time()
# Check cache first (cache-aside pattern)
cached_data = r.get(cache_key)
if cached_data:
# Cache hit
elapsed = (time.time() - start_time) * 1000
r.hincrby("cache:stats", "hits", 1)
r.hincrby("cache:stats", "requests", 1)
print(f"CACHE HIT for {username} - {elapsed:.2f}ms")
return jsonify(json.loads(cached_data))
# Cache miss - fetch from GitHub
response = requests.get(f"https://api.github.com/users/{username}")
if response.status_code != 200:
return jsonify({"error": "User not found"}), 404
user_data = response.json()
result = {
"username": user_data["login"],
"name": user_data["name"],
"public_repos": user_data["public_repos"],
"followers": user_data["followers"],
"bio": user_data["bio"],
}
# Store in cache with TTL
r.setex(cache_key, CACHE_TTL, json.dumps(result))
elapsed = (time.time() - start_time) * 1000
r.hincrby("cache:stats", "misses", 1)
r.hincrby("cache:stats", "requests", 1)
print(f"CACHE MISS for {username} - {elapsed:.2f}ms")
return jsonify(result)
@app.route("/github/<username>/cache", methods=["DELETE"])
def invalidate_cache(username):
cache_key = f"github:{username}"
deleted = r.delete(cache_key)
if deleted:
return jsonify({"message": f"Cache cleared for {username}"})
return jsonify({"message": f"No cache entry found for {username}"})
if __name__ == "__main__":
app.run(debug=True)
Test the full caching lifecycle
Now you have all the pieces to walk through the complete caching lifecycle. You'll make requests, observe cache behavior, manually invalidate, and confirm the cycle resets.
- In your browser, visit http://localhost:5000/github/octocat.
- Check your VS Code terminal. You should see CACHE MISS with a response time in the hundreds of milliseconds. This is the first request fetching fresh data from GitHub.
- Refresh the same page in your browser.
- Check your VS Code terminal again. You should see CACHE HIT with a response time under 1 millisecond. The data came from Redis this time.
Now invalidate the cache manually. Your browser can't send DELETE requests, so you'll use PowerShell.
- Open a second terminal in VS Code by clicking the + icon in the terminal panel.
- Send a DELETE request to invalidate the cache by running:
curl.exe -X DELETE http://localhost:5000/github/octocat/cache
You should see a JSON response: {"message":"Cache cleared for octocat"}.
- Go back to your browser and refresh http://localhost:5000/github/octocat one more time.
- Check your first terminal (where Flask is running). You should see CACHE MISS again. The cache was invalidated, so your app had to fetch fresh data from GitHub.
Why is manual invalidation useful?
In production, you might need to force a cache refresh when upstream data changes. For example, if a user updates their GitHub profile, you don't want to serve stale data for the next 60 seconds.
The DELETE endpoint gives you on-demand control without waiting for the TTL to expire naturally.
Getting a 404 or connection error from curl.exe?
Make sure your Flask app is still running in the first terminal. If you accidentally closed it, run python app.py again. Also check that you're using curl.exe (not just curl) in PowerShell, since curl is an alias for a different command on Windows.
help me send a DELETE request to my Flask app
You now have a complete, production-style caching system. Your API automatically caches responses, expires them after 60 seconds, and lets you manually clear entries on demand. Next up, you'll extend this into a statistics dashboard in the Secret Mission.
Secret mission
Cache Statistics Dashboard
Your caching layer works, but how do you know how well it's performing? Build a real-time statistics dashboard that tracks every cache hit and miss, then calculates your hit rate percentage.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Redis server running inside WSL2.
- Project folder with virtual environment and app.py.
- WSL2 Ubuntu distribution.
Keep everything running
No action needed. Choose this if you're still actively building or want to keep experimenting with Redis caching.
- Your Redis server will continue running inside WSL2 until you shut it down or restart your computer.
- Your project folder and virtual environment stay on your Desktop.
- VS Code and Python remain installed on your machine.
Pause - I'll come back to this later
Shut down Redis to free up memory, but keep all your files so you can pick up where you left off.
- Open your Ubuntu terminal from the Start menu.
- Stop the Redis server by running:
redis-cli shutdown
- When you come back, start Redis again by opening Ubuntu and running:
redis-server --daemonize yes
- Your project folder and virtual environment remain intact on your Desktop.
Delete - I don't want to use this again
Remove all project resources and start fresh. This removes Redis, your project files, and the WSL2 Ubuntu distribution.
- Open your Ubuntu terminal and stop Redis:
redis-cli shutdown
- Remove Redis from Ubuntu by running:
sudo apt-get remove redis
- Close the Ubuntu terminal.
- Delete your project folder. Open PowerShell in VS Code and run:
Remove-Item -Recurse -Force ~\Desktop\redis-cache-api
- To remove WSL2 Ubuntu entirely, open PowerShell as Administrator and run:
wsl --unregister Ubuntu
What does unregistering WSL do?
This removes the Ubuntu distribution and all data stored inside it (including the Redis installation). It does not uninstall WSL itself from Windows.
That's a Wrap!
That's a Wrap!
Nice work! You just built a production-style caching layer that proves how much faster cached responses are compared to raw API calls.
You've learned how to:
- Built a Flask API that fetches live data from the GitHub API and returns structured JSON responses.
- Implemented the cache-aside pattern with Redis, using TTL-based expiration to keep cached data fresh.
- Measured the performance difference between cached and uncached responses, and added manual cache invalidation for on-demand cache clearing.
- Built a real-time cache statistics dashboard tracking hits, misses, and hit rate percentage.
Ready to quiz yourself?