Build a Game Stats Tracker with Redis
Learn Redis data structures by building a Python CLI game stats tracker.
Introduction
30 Second Summary
Think about leaderboards in games. When you finish a match and your rank updates instantly, that speed comes from an in-memory data store doing its job behind the scenes.
In this project, you will build a Game Stats Tracker in Python that talks to a local Redis server. You will store player scores, organize profiles, build a live leaderboard, and watch sessions expire on their own.
What You'll Build
You run your script and watch it store scores in sub-millisecond time, pull full player profiles in one shot, rank players automatically, and count down a session key until it vanishes.
By the end of this project, you'll have:
- A working score system that stores and retrieves player data instantly using Redis strings, with atomic increments that update values without race conditions.
- Structured player profiles you can fetch as a single dictionary or update one field at a time, using Redis hashes.
- A live leaderboard that ranks players automatically and answers "who are the top 3?" without you writing any sorting logic, using sorted sets.
- Self-expiring session keys that disappear after a countdown without any cleanup code, using TTL.
- Secret Mission: a rate limiter that blocks excessive requests using atomic counters and TTL.
Are there any prerequisites?
You should have basic Python knowledge (variables, functions, print statements) and be comfortable running commands in a terminal. No prior Redis experience is needed.
Before We Start
Before We Start...
Before we dive in, let's lock in what you're about to build and why it matters. This is where you commit to the goal so every step ahead has clear purpose.
Set Up Redis and Python
Your Game Stats Tracker needs two things before it can do anything: a running Redis server to store data, and a Python environment that can talk to it.
In this step, you'll install Redis on your Mac using Homebrew, create a fresh Python project with the redis-py client library, and verify the connection works end to end.
In this step, get ready to:
- Install Redis and confirm it responds to commands.
- Create a Python project with the redis-py library installed.
- Write connection code and verify it prints a successful ping.
Install Redis via Homebrew
Redis is installed via Homebrew on macOS. Homebrew is a package manager that lets you install tools with a single terminal command.
- Open your terminal by pressing Cmd+Space to open Spotlight, typing Terminal, and pressing Enter.
- Check if Homebrew is already installed by running this command:
brew --version
✔️ I see a version number
Homebrew is ready. Move on to installing Redis below.
ⓧ Command not found
Homebrew is not installed yet. Install it by running this command:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
What does this command do?
This downloads and runs the official Homebrew installer script. It will ask for your Mac password during installation. Follow the prompts in your terminal until it completes.
- After installation completes, close and reopen your terminal.
- Run brew --version again to confirm it prints a version number.
- Install Redis by running this command:
brew install redis
Homebrew downloads and installs Redis. You'll see progress output as it works.
- Start Redis as a background service by running:
brew services start redis
What does this command do?
This launches Redis in the background and registers it to restart automatically when you log in. You won't see Redis running in your terminal because it operates as a background process.
- Verify Redis is running by running:
redis-cli ping
You should see PONG printed in your terminal. This confirms Redis is alive and accepting connections.
Not seeing PONG?
- If you see Could not connect to Redis, the service may not have started. Run brew services restart redis and try again.
- If you see command not found: redis-cli, the install may not have completed. Run brew install redis again.
Help me troubleshoot why redis-cli ping is not returning PONG on my Mac after running brew services start redis.
Set up the Python project
With Redis running, you need a Python project that can connect to it. You'll create a dedicated folder, a virtual environment to keep dependencies isolated, and install the redis-py client library.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir redis-game-tracker
cd redis-game-tracker
- Confirm you're inside the new folder by running:
pwd
You should see a path ending in redis-game-tracker.
- Create and activate a Python virtual environment by running these commands:
python3 -m venv venv
source venv/bin/activate
Your terminal prompt now shows (venv) at the beginning. This means the virtual environment is active and any packages you install stay inside this project.
Don't see (venv) in your prompt?
- Make sure you ran both lines. The first line creates the environment. The second line activates it.
- If you see an error about python3 not found, check that Python 3 is installed by running python3 --version.
Help me figure out why my Python virtual environment isn't activating after running source venv/bin/activate.
- Install the redis-py library by running:
pip install redis==8.0.1
You'll see download and installation output. This installs the official Python client for Redis at version 8.0.1.
- Create the main project file by running:
touch game_tracker.py
- Confirm the file exists by running:
ls
You should see game_tracker.py and venv listed in the output.
Connect to Redis from Python
Now you'll write the code that connects your Python app to the Redis server running in the background. This is the bridge between your code and the data store.
- Open VS Code by pressing Cmd+Space (macOS) or the Windows key (Windows), typing VS Code, and pressing Enter.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to Desktop, select the redis-game-tracker folder, then click Open.
- In the Explorer sidebar on the left, click game_tracker.py to open it.
- Paste the following code into game_tracker.py:
import redis
import time
# Connect to Redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
# Verify connection
print("Connected to Redis:", r.ping())
What does this code do?
- `import redis` loads the redis-py client library you just installed.
- `import time` is loaded now for use in later steps (session expiration countdowns).
- `redis.Redis(host="localhost", port=6379, decode_responses=True)` creates a connection to your local Redis server. The decode_responses=True flag tells redis-py to return strings instead of raw bytes.
- `r.ping()` sends a PING command to Redis and returns True if the server responds.
- Save game_tracker.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Before you run this script, what do you think the output will be? Will the connection succeed or fail?
- Switch back to your terminal (where the virtual environment is active) and run the script:
python3 game_tracker.py
You should see:
Connected to Redis: True
Your Python app successfully talked to Redis. The connection is working.
Seeing a ConnectionError?
- Make sure Redis is still running. Run redis-cli ping in your terminal. If it does not return PONG, restart Redis with brew services restart redis.
- Make sure your virtual environment is active. You should see (venv) in your prompt. If not, run source venv/bin/activate from inside the redis-game-tracker folder.
- If you see ModuleNotFoundError: No module named 'redis', the library is not installed in your active environment. Run pip install redis==8.0.1 again.
Help me fix a ConnectionError when running my Python script that connects to Redis on localhost.
✔️ Awesome, I've got everything!
Great. Double check you've saved game_tracker.py and confirmed the output reads Connected to Redis: True.
ⓧ I'd like to double check the full code
Here is the complete game_tracker.py file at this point:
import redis
import time
# Connect to Redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
# Verify connection
print("Connected to Redis:", r.ping())
Your environment is ready. Redis is running, Python can talk to it, and you have a project folder ready for the real work. Next up, you'll store your first data in Redis using strings.
Store Player Scores with Strings
Your Redis server is running and Python can talk to it. Now you need to actually store data and get it back. Strings are Redis's simplest data structure. They let you store a single value under a key and retrieve it instantly.
In this step, you will use strings to store player scores, retrieve them, and update them atomically. This gives you the first visible proof that Redis is working as your data store.
In this step, get ready to:
- Store and retrieve player scores using SET and GET.
- Update a score atomically using INCR.
- Discover the limitation of using separate string keys for related data.
SET and GET basics
Every key-value store works on the same principle: you store a value under a name, then ask for it back by that name. Redis strings are exactly this pattern.
- In game_tracker.py, add this code below the print("Connected to Redis:", r.ping()) line:
# --- STRINGS: Basic score storage ---
def store_scores():
"""Store and retrieve simple player scores using strings."""
print("\n--- Strings: Basic Scores ---")
# SET and GET
r.set("score:alice", 1500)
r.set("score:bob", 2200)
r.set("score:charlie", 1800)
# Retrieve scores
alice_score = r.get("score:alice")
print(f"Alice's score: {alice_score}")
bob_score = r.get("score:bob")
print(f"Bob's score: {bob_score}")
charlie_score = r.get("score:charlie")
print(f"Charlie's score: {charlie_score}")
What does this code do?
This defines a function that demonstrates the two most fundamental Redis commands.
- r.set("score:alice", 1500) stores the value 1500 under the key score:alice. The colon is just a naming convention to group related keys.
- r.get("score:alice") retrieves whatever value is stored at that key.
The function exists but nothing calls it yet. You need a main block to run it.
- Add this code at the very bottom of game_tracker.py, below the store_scores() function:
# --- Run everything ---
if __name__ == "__main__":
# Clear previous run data
r.flushdb()
print("Redis Game Stats Tracker")
print("=" * 40)
store_scores()
print("\n" + "=" * 40)
print("Done! All Redis data structures explored.")
print(f"Total keys in database: {r.dbsize()}")
What does the main block do?
- r.flushdb() clears all keys in the current Redis database. This ensures a clean slate every time you run the script.
- The if __name__ == "__main__": guard means this code only runs when you execute the file directly, not when it is imported.
- Save game_tracker.py.
- Run your script to confirm scores are stored and retrieved:
python3 game_tracker.py
You should see the three player scores printed under the "Strings: Basic Scores" heading.
Not seeing scores printed?
- Make sure the store_scores() function definition is ABOVE the if __name__ == "__main__": block. Python reads top to bottom.
- Check that the if __name__ block calls store_scores() with matching spelling.
- Confirm Redis is still running with redis-cli ping. You should see PONG.
Help me debug my store_scores function
Atomic increment with INCR
Imagine two game servers try to add points to Alice at the same time. With a normal read-modify-write, one update could overwrite the other. Redis atomic operations solve this by executing the increment in a single step that no other command can interrupt.
- In game_tracker.py, find the last print(f"Charlie's score: {charlie_score}") line inside store_scores().
- Add this code directly below it, still inside the function:
# INCR - atomic increment
r.incr("score:alice", 100)
print(f"\nAlice's score after +100 bonus: {r.get('score:alice')}")
# Check if a key exists
print(f"\nDoes 'score:alice' exist? {r.exists('score:alice')}")
print(f"Does 'score:nobody' exist? {r.exists('score:nobody')}")
What does this code do?
- r.incr("score:alice", 100) adds 100 to Alice's score in a single atomic step. No read, no separate write. No race condition possible.
- r.exists("score:alice") returns 1 if the key exists and 0 if it does not. Useful for checking state without fetching the value.
- Save game_tracker.py and run it again:
python3 game_tracker.py
You should now see Alice's score jump from 1500 to 1600 after the bonus, plus the exists checks printing 1 and 0.
Alice's score showing 1500 instead of 1600?
- Make sure the r.incr() line appears AFTER the r.set("score:alice", 1500) line. The increment must happen after the initial value is set.
- Check indentation. The INCR code must be inside the store_scores() function (indented by 4 spaces).
Help me fix my INCR code
See the limitation
You now have three separate keys: score:alice, score:bob, and score:charlie. What if you need to store a player's name, score, level, AND class? You would need four separate keys per player, with no way to fetch them as a group.
- Add these lines at the end of the store_scores() function, below the exists checks:
print("\n[!] But what if we need name, score, level, AND class per player?")
print(" We'd need 4 separate keys per player. That's messy.")
print(" Solution: hashes (next section).")
Why call this out in the code?
These print statements make the script self-documenting. When you run it, the output narrates the journey from one data structure to the next. This limitation is real: strings store one value per key, so grouping related fields requires a different tool.
Before you run this, what do you think the full output will look like now that all three sections are in place?
- Save game_tracker.py and run it:
python3 game_tracker.py
You should see the full output: player scores, Alice's updated score of 1600 after the bonus, the exists checks, and the limitation message pointing toward hashes.
Seeing a SyntaxError or IndentationError?
- All code inside store_scores() must be indented by exactly 4 spaces. Mixing tabs and spaces causes errors.
- The print statements you just added must be at the same indent level as the other print statements in the function.
Help me fix my indentation error
✔️ 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 game_tracker.py at this point. Scroll up and confirm each section is present in order: the connection code at the top, the store_scores() function in the middle, and the __main__ block at the bottom.
import redis
import time
# Connect to Redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
# Verify connection
print("Connected to Redis:", r.ping())
# --- STRINGS: Basic score storage ---
def store_scores():
"""Store and retrieve simple player scores using strings."""
print("\n--- Strings: Basic Scores ---")
# SET and GET
r.set("score:alice", 1500)
r.set("score:bob", 2200)
r.set("score:charlie", 1800)
# Retrieve scores
alice_score = r.get("score:alice")
print(f"Alice's score: {alice_score}")
bob_score = r.get("score:bob")
print(f"Bob's score: {bob_score}")
charlie_score = r.get("score:charlie")
print(f"Charlie's score: {charlie_score}")
# INCR - atomic increment
r.incr("score:alice", 100)
print(f"\nAlice's score after +100 bonus: {r.get('score:alice')}")
# Check if a key exists
print(f"\nDoes 'score:alice' exist? {r.exists('score:alice')}")
print(f"Does 'score:nobody' exist? {r.exists('score:nobody')}")
print("\n[!] But what if we need name, score, level, AND class per player?")
print(" We'd need 4 separate keys per player. That's messy.")
print(" Solution: hashes (next section).")
# --- Run everything ---
if __name__ == "__main__":
# Clear previous run data
r.flushdb()
print("Redis Game Stats Tracker")
print("=" * 40)
store_scores()
print("\n" + "=" * 40)
print("Done! All Redis data structures explored.")
print(f"Total keys in database: {r.dbsize()}")
Strings gave you the basic store-and-retrieve loop, plus atomic updates. But you have already hit the wall: one key holds one value. In the next step, you will use Redis hashes to store an entire player profile under a single key.
Organize Player Profiles with Hashes
In the last step, you stored each player's score as a separate string key. That works fine for a single value, but players have more data than just a score.
What about their name, level, and character class? Storing each piece as its own key means four separate keys per player and no way to fetch them as a group. Redis hashes solve this by letting you store multiple fields under a single key, exactly like a Python dictionary.
In this step, get ready to:
- Store complete player profiles (name, score, level, class) in Redis hashes.
- Retrieve full profiles and individual fields from a hash.
- Update a single field without rewriting the entire profile.
Store structured player data
A Redis hash maps a single key to a set of field-value pairs. Instead of score:alice, name:alice, level:alice as separate keys, you store everything under one key: player:alice.
- In game_tracker.py, find the comment # --- Run everything --- and add the following code above it:
# --- HASHES: Player profiles ---
def store_profiles():
"""Store structured player data using hashes."""
print("\n--- Hashes: Player Profiles ---")
# Store multiple fields for one player in a single key
r.hset("player:alice", mapping={
"name": "Alice",
"score": "1600",
"level": "12",
"class": "Warrior",
})
r.hset("player:bob", mapping={
"name": "Bob",
"score": "2200",
"level": "18",
"class": "Mage",
})
r.hset("player:charlie", mapping={
"name": "Charlie",
"score": "1800",
"level": "15",
"class": "Rogue",
})
# Get one field
print(f"Alice's class: {r.hget('player:alice', 'class')}")
# Get all fields for a player
profile = r.hgetall("player:alice")
print(f"Alice's full profile: {profile}")
What does this code do?
- r.hset("player:alice", mapping={...}) stores four fields (name, score, level, class) under one key. One key, multiple fields.
- r.hget('player:alice', 'class') pulls back a single field from the hash without fetching everything else.
- r.hgetall("player:alice") returns the entire hash as a Python dictionary. Every field and value comes back in one call.
Now you need to call this function from your main block so it actually runs.
- In the if __name__ == "__main__": block at the bottom of the file, find this line:
store_scores()
- Add store_profiles() directly below it, so the block now looks like this:
store_scores()
store_profiles()
- Save game_tracker.py.
- Run the script to confirm profiles are stored and retrieved:
python3 game_tracker.py
In the output, you should see Alice's class: Warrior and a full profile dictionary printed with all four fields (name, score, level, class).
Not seeing hash output?
- Make sure store_profiles() is indented inside the if __name__ == "__main__": block, at the same level as store_scores().
- Check that the store_profiles() function definition sits above the # --- Run everything --- comment, not inside another function.
- If you see NameError: name 'store_profiles' is not defined, the function definition is below the __main__ block. Move it above.
Help me debug my store_profiles function
Read and update individual fields
One of the biggest advantages of hashes is that you can update a single field without rewriting the entire object. Try updating Alice's level without touching her name, score, or class.
- In the store_profiles() function, find the line print(f"Alice's full profile: {profile}") and add the following code below it:
# Update a single field without touching others
r.hset("player:alice", "level", "13")
print(f"Alice leveled up! New level: {r.hget('player:alice', 'level')}")
What does this code do?
- r.hset("player:alice", "level", "13") overwrites just the level field. Alice's name, score, and class stay exactly as they were.
- This is O(1) per field. No need to read the whole hash, change one value, and write it all back.
- Save game_tracker.py and run the script again:
python3 game_tracker.py
You should now see Alice leveled up! New level: 13 printed after the full profile output.
Level not updating?
- Make sure the new code is inside the store_profiles() function body (indented with 4 spaces), not at the module level.
- Check that "13" is a string in quotes. Redis stores hash values as strings.
Why isn't my hash field updating?
Recognize the next limitation
You now have structured profiles. But there is a question hashes cannot answer efficiently: who has the highest score?
- Still inside store_profiles(), add the following below the level-up print statement:
print("\n[!] Great, we have structured profiles.")
print(" But who has the HIGHEST score?")
print(" With hashes, we'd fetch every player and sort manually.")
print(" Solution: sorted sets (next section).")
Why is this a limitation?
To find the top scorer, you would need to call r.hget() on every single player hash, pull their score, and sort the results yourself in Python. With 10 players that is fine. With 10,000 players it becomes expensive. Redis has a data structure built for exactly this problem.
Before you run the final version, what do you think the output will show after the profiles section?
- Save game_tracker.py and run the script:
python3 game_tracker.py
You should see the complete hash section output: Alice's class as Warrior, her full profile printed as a dictionary, the level-up confirmation showing 13, and the limitation message pointing to sorted sets.
✔️ Awesome, I've got everything!
Great. Double check you have saved game_tracker.py and the output shows all hash operations working.
ⓧ I'd like to double check the full code
Your game_tracker.py file is now longer than 30 lines. Scroll up and confirm each section is present in this order:
- The import redis and import time lines at the top.
- The Redis connection block (r = redis.Redis(...) and r.ping()).
- The store_scores() function from Step 2.
- The store_profiles() function you just added, with HSET mapping calls for three players, HGET, HGETALL, level update, and limitation prints.
- The if __name__ == "__main__": block calling both store_scores() and store_profiles().
Your players now have full profiles stored as structured data. Next up, you will build a leaderboard that automatically ranks players by score using sorted sets.
Build a Leaderboard with Sorted Sets
Your player profiles are nicely organized in hashes. But try answering this question: who has the highest score? With hashes, you would need to fetch every single player's score and sort them yourself in Python.
Redis sorted sets solve this problem. They maintain order automatically. Adding a score is O(log n), and getting the top players is instant. This is the data structure that makes Redis the go-to choice for leaderboards.
In this step, get ready to:
- Add players to a sorted set with their scores as the ranking key.
- Query player rankings and update scores atomically.
- Run range queries to count players within a score bracket.
Add players to the leaderboard
A sorted set stores members alongside a numeric score. Redis uses that score to keep every member in order at all times. You never have to sort manually.
- In game_tracker.py, find the comment # --- Run everything --- near the bottom of the file.
- Add the following function directly above that comment:
# --- SORTED SETS: Leaderboard ---
def build_leaderboard():
"""Build a real-time leaderboard using sorted sets."""
print("\n--- Sorted Sets: Leaderboard ---")
# Add players with scores (score is the sort key)
r.zadd("leaderboard", {"alice": 1600, "bob": 2200, "charlie": 1800})
# Get all players ranked highest to lowest
top_players = r.zrevrange("leaderboard", 0, -1, withscores=True)
print("Leaderboard (top to bottom):")
for rank, (player, score) in enumerate(top_players, 1):
print(f" #{rank} {player}: {int(score)} pts")
What does this code do?
- r.zadd("leaderboard", {...}) adds all three players to a sorted set called leaderboard. Each player's number is the score Redis uses to sort them.
- r.zrevrange("leaderboard", 0, -1, withscores=True) retrieves all members from highest score to lowest. The rev in the name means "reverse" (descending order).
- withscores=True returns each player as a (name, score) tuple so you can display both.
Now you need to call this function so it actually runs.
- Scroll down to the if __name__ == "__main__": block.
- Find the line that says:
store_profiles()
- Add build_leaderboard() on the next line, so it looks like this:
store_scores()
store_profiles()
build_leaderboard()
- Save game_tracker.py.
- Run the script to see your leaderboard in action:
python3 game_tracker.py
In the output, look for the new "Sorted Sets: Leaderboard" section. You should see Bob at #1 with 2200 pts, Charlie at #2 with 1800 pts, and Alice at #3 with 1600 pts.
Not seeing the leaderboard section?
- Make sure you added the build_leaderboard() call inside the if __name__ == "__main__": block, not outside it.
- Check that your function definition starts at the same indentation level as store_scores() and store_profiles() (no extra indentation).
- Help me debug my leaderboard function
Query and update rankings
You have a ranked list, but a real leaderboard needs more. You need to look up a specific player's rank and update scores without rebuilding the whole set.
- In game_tracker.py, find the last line of your build_leaderboard() function (the print(f" #{rank}... line inside the for loop).
- Add the following code below the for loop, still inside the function:
# Get a specific player's rank (0-indexed from top)
alice_rank = r.zrevrank("leaderboard", "alice")
print(f"\nAlice's rank: #{alice_rank + 1}")
# Give Alice a 500-point bonus (atomic increment)
r.zincrby("leaderboard", 500, "alice")
new_score = r.zscore("leaderboard", "alice")
print(f"Alice gets +500 bonus! New score: {int(new_score)}")
# Show updated leaderboard
top_players = r.zrevrange("leaderboard", 0, -1, withscores=True)
print("\nUpdated leaderboard:")
for rank, (player, score) in enumerate(top_players, 1):
print(f" #{rank} {player}: {int(score)} pts")
What does this code do?
- r.zrevrank("leaderboard", "alice") returns Alice's position in the set, ranked highest-to-lowest. It is 0-indexed, so you add 1 for a human-readable rank.
- r.zincrby("leaderboard", 500, "alice") atomically adds 500 to Alice's score. No read-modify-write cycle needed. Redis handles concurrency for you.
- r.zscore("leaderboard", "alice") fetches Alice's current score to confirm the update.
- Save game_tracker.py and run the script again:
python3 game_tracker.py
You should see Alice's rank displayed as #3, then after the 500-point bonus her new score of 2100 pts. The updated leaderboard shows Alice climbing from #3 to #2.
Why atomic operations matter here
In a real game, hundreds of players might earn points at the same time. If you had to read a score, add to it in Python, then write it back, two simultaneous updates could overwrite each other.
ZINCRBY does the entire read-add-write in a single Redis operation. No data gets lost, even under heavy concurrent access.
Seeing a TypeError or None for the rank?
- Make sure the player name in zrevrank matches exactly what you passed to zadd. Spelling and case matter.
- Check that the new code is inside the build_leaderboard() function (indented with 4 spaces), not at the top level of the file.
- Help me fix my ranking code
Run range queries
Sorted sets can also answer questions like "how many players scored between 1500 and 2000?" without scanning every member. Redis uses a skip list internally, making these range queries O(log n).
- In game_tracker.py, add the following code at the end of your build_leaderboard() function, after the updated leaderboard for loop:
# Count players in a score range
mid_tier = r.zcount("leaderboard", 1500, 2000)
print(f"\nPlayers scoring 1500-2000: {mid_tier}")
What does this code do?
r.zcount("leaderboard", 1500, 2000) counts how many members have a score between 1500 and 2000 (inclusive). Redis answers this from its internal skip list structure without iterating through every player.
- Save game_tracker.py.
Before you run the script, predict: how many of the three players will fall in the 1500-2000 range after Alice's 500-point bonus?
- Run the script to check your prediction:
python3 game_tracker.py
You should see "Players scoring 1500-2000: 1". Only Charlie (1800) falls in that range. Alice jumped to 2100 and Bob is at 2200, so both are above the range.
Seeing 0 or an unexpected count?
- Remember that the script calls r.flushdb() at the top of each run. The count reflects scores AFTER Alice's 500-point bonus in the same run, not scores from a previous run.
- Make sure the zcount line is after the zincrby line in your function. Order matters.
- Help me understand my zcount result
✔️ 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
Your game_tracker.py is now over 30 lines, so here is a checklist to confirm everything is in place. Scroll through your file and verify:
- The import and connection block at the top (import redis, import time, create client, print ping).
- The store_scores() function with SET, GET, INCR, and exists checks.
- The store_profiles() function with HSET for three players, HGET, HGETALL, and a level update.
- The build_leaderboard() function with ZADD, ZREVRANGE, ZREVRANK, ZINCRBY, ZSCORE, a second ZREVRANGE, and ZCOUNT.
- The if __name__ == "__main__": block calling store_scores(), store_profiles(), and build_leaderboard() in that order.
Your leaderboard updates rankings automatically whenever a score changes. Next up, you will learn how to make data disappear on its own using TTL. This is the pattern behind session management and caching.
Auto-Expire Data with TTL
You now have strings, hashes, and sorted sets working. But Redis stores everything in memory. Without expiration, old data accumulates forever and eventually crashes the server.
TTL (Time To Live) makes keys disappear automatically after a countdown. This is the core pattern behind caching, sessions, and rate limiting.
In this step, get ready to:
- Create a session key that expires on its own after 10 seconds.
- Watch the TTL countdown tick down and the key vanish.
- Add a TTL retroactively to a key that was created without one.
Create a key with built-in expiration
Redis lets you attach a countdown timer to any key when you create it. Once the timer hits zero, the key is deleted automatically.
- In game_tracker.py, find the comment # --- Run everything ---.
- Add the following function directly above that comment:
# --- TTL: Session expiration ---
def manage_sessions():
"""Demonstrate TTL-based expiration for sessions."""
print("\n--- TTL: Session Expiration ---")
# Create a session that expires in 10 seconds
session_id = "session:user42"
r.set(session_id, "logged_in", ex=10)
print(f"Session created: {r.get(session_id)}")
print(f"Time remaining: {r.ttl(session_id)} seconds")
What does this code do?
The ex=10 parameter on r.set() tells Redis to delete this key automatically after 10 seconds. No cleanup code needed.
The r.ttl() command returns how many seconds remain before the key disappears.
Watch the countdown and add TTL to an existing key
Now you will wait a few seconds, re-check the TTL to see it ticking down, and also learn how to add a TTL to a key that was created without one.
- Add the following code at the bottom of your manage_sessions() function, directly below the last print line you just wrote:
# Wait and check again
print("\nWaiting 3 seconds...")
time.sleep(3)
print(f"Session still active: {r.get(session_id)}")
print(f"Time remaining: {r.ttl(session_id)} seconds")
# Add TTL to an existing key
r.set("temp:data", "this will expire")
r.expire("temp:data", 5)
print(f"\nAdded 5s TTL to 'temp:data'. TTL: {r.ttl('temp:data')}s")
# Wait for session to expire
print("\nWaiting 8 seconds for session to expire...")
time.sleep(8)
expired_value = r.get(session_id)
expired_ttl = r.ttl(session_id)
print(f"Session value after expiry: {expired_value}")
print(f"TTL of expired key: {expired_ttl} (-2 means key doesn't exist)")
What does this code do?
- time.sleep(3) pauses the script so you can observe the TTL ticking down. After 3 seconds, the session is still alive but the remaining time is lower.
- r.set("temp:data", "this will expire") creates a key with no expiration. Then r.expire("temp:data", 5) adds a 5-second TTL retroactively. This is how you add expiration to keys that already exist.
- After waiting 8 more seconds, the original session key is gone. r.get() returns None and r.ttl() returns -2 (meaning the key does not exist at all).
Why every cache key needs an expiration
In production, you set TTLs on cache keys to prevent unbounded memory growth. Without them, stale sessions, old cache entries, and temporary data pile up until Redis runs out of RAM.
Run and see keys expire in real time
Your function is ready. Now you need to call it from the main block so it runs when you execute the script.
- In the if __name__ == "__main__": block at the bottom of your file, find this line:
build_leaderboard()
- Add manage_sessions() on the next line, so the block now looks like this:
store_scores()
store_profiles()
build_leaderboard()
manage_sessions()
- Save game_tracker.py.
Before you run this, what do you think will happen to the session key after 10 seconds pass? Will it still be there, or will it be gone?
- Run your script by entering this command in your terminal:
python3 game_tracker.py
The script takes about 11 seconds to finish because of the two sleep calls. You should see the TTL section showing a session created, the TTL decreasing after 3 seconds, and then the key returning None after it expires.
Session not expiring?
- Make sure the indentation of your new code is consistent with the rest of the function. Every line inside manage_sessions() should be indented with 4 spaces.
- Confirm that manage_sessions() is called inside the if __name__ == "__main__": block, not outside it.
- If you see a NameError about time, check that import time is at the top of your file (it should already be there from a previous step).
Help me debug my manage_sessions() function
✔️ Awesome, I've got everything!
Great. Double check you have saved game_tracker.py and that you see the TTL countdown output in your terminal.
ⓧ I'd like to double check the full code
Here is the complete game_tracker.py up to this point. Scroll through and confirm each section is present in this order:
- Imports and connection at the top.
- store_scores() function (strings section).
- store_profiles() function (hashes section).
- build_leaderboard() function (sorted sets section).
- manage_sessions() function (TTL section you just added).
- The if __name__ == "__main__": block calling all four functions.
The full file is over 30 lines, so scroll up through your earlier chunks to confirm each one is present and in order.
You have now used four core Redis data structures. You know how to store simple values, structured objects, ranked data, and self-expiring keys. Next up, you will put everything together and see the full picture of your game tracker in action.
Secret mission
Build a Rate Limiter
Rate limiting is one of Redis's most common production use cases. Can you combine INCR and TTL into a function that blocks excessive requests and resets automatically?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- A local Redis server running as a background service via Homebrew.
- The redis-game-tracker project folder on your Desktop (contains game_tracker.py and the venv virtual environment).
Keep everything running
No action needed. Choose this if you're still actively building or plan to explore Redis further.
- Redis uses very little memory when idle. Leave it running for future projects.
- Your redis-game-tracker folder stays on your Desktop ready to reuse.
- Homebrew remains installed as a general-purpose package manager for future tools.
Pause - I'll come back to this later
Stop the Redis background service to free up memory. Your project files and Redis installation stay intact for when you return.
- Stop the Redis service by running:
brew services stop redis
- Deactivate your Python virtual environment by running:
deactivate
When you come back, restart Redis with brew services start redis and reactivate your virtual environment with source venv/bin/activate.
Delete - I don't want to use this again
Remove all project resources and start fresh. This stops Redis, uninstalls it, and deletes your project files.
- Stop the Redis service and uninstall Redis by running:
brew services stop redis
brew uninstall redis
- Deactivate your virtual environment (if still active) by running:
deactivate
- Delete the project folder by running:
cd ~/Desktop
rm -rf redis-game-tracker
- Confirm the folder is gone by running:
ls ~/Desktop
You should no longer see redis-game-tracker in the output.
That's a Wrap!
That's a Wrap!
Nice work. You just built a fully working Game Stats Tracker backed by Redis, exploring four core data structures and seeing each one solve a different problem in real time.
You've learned how to:
- Connected Python to a local Redis server and used strings, hashes, sorted sets, and TTL to solve four distinct data access patterns.
- Built a real-time leaderboard with sorted sets that ranks players automatically and updates in O(log n) time.
- Used TTL expiration to make session keys disappear on their own, the core pattern behind caching and session management in production.
- Built a rate limiter combining atomic counters and TTL into a production-ready pattern that blocks excessive requests (Secret Mission).
Ready to quiz yourself?