
Rift Rewind - Querying Riot Games API For Match Data Via AWS Lambda
Replay your League matches through the cloud - one Lambda function at a time.
Rift Rewind Developer Challenge #2 - Day 1
Welcome to Day 1 of Challenge #2! Today you'll build a system that fetches League of Legends match data from the Riot Games API and stores it securely in S3. Think of this as creating your own personal match history database - perfect for tracking your climb, analyzing what's working, or figuring out why you're hardstuck.In about 60 minutes, you'll learn how to securely store API credentials, write serverless functions that fetch game data, and save that data to cloud storage. We're not just calling an API - we're building a data pipeline that could scale to analyze thousands of matches (or just prove to your duo that you're not the one inting).By the end of today, you'll have a Lambda function that fetches match data and stores it in S3, ready for future analysis. Time to start collecting data like the pros prepping for scrims!
What You Will Accomplish Today
✅ Register for a Riot Games Developer API key
✅ Securely store your API key in AWS Parameter Store
✅ Create an S3 bucket for storing match data
✅ Set up Lambda permissions (the easy way for learning)
✅ Write a Lambda function that fetches match history
✅ Extract champion, items, and performance stats from matches
✅ Save match data as JSON files in S3
✅ Understand data storage costs and limits
What You Will Need
- Your AWS account with IAM admin user 💻
- Riot Games account 🎮
- About 60 minutes ⏰
- A Riot ID to test with (GameName#TAG format) 🔍
Step 1: Get Your Riot Developer API Key 🔑
First, you need access to the Riot Games API. The development key is perfect for learning and building small projects - think of it like getting practice tool access before you take it to ranked.
Register for Riot Developer Portal
- Go to Riot Developer Portal - Visit developer.riotgames.com
- Sign in - Use your existing Riot Games account (or create one if needed)
- Accept Terms - Read and accept the Riot Games API Terms of Service
- Get your Development API Key:
- Navigate to your dashboard
- You'll see "Development API Key" with a "REGENERATE API KEY" button
- Copy this key and save it securely (it expires every 24 hours, so you'll need to regenerate it tomorrow)
Important: Development keys have rate limits of 20 requests every second and 100 requests every 2 minutes. This is plenty for learning - you're not trying to download Faker's entire match history in one go.
Understanding What Data We'll Fetch
We'll fetch a summoner's recent match history - specifically the last 5-10 matches. This includes:
- Match IDs and timestamps
- Champion played and final item build
- Game mode and duration
- Performance stats (KDA, CS, gold, damage)
- Win/loss results
- Role and position information
This is a manageable dataset (typically a few KB per match) that won't take up your storage. You could store thousands of matches before running into limits.
About Riot IDs
Riot Games now uses Riot IDs in the format
GameName#TAG (like "Hide on bush#KR1"). This replaced the old summoner name system. You'll need to use this format when testing your Lambda function.
Step 2: Create Your Data Storage Bucket 📦
Let's create an S3 bucket specifically for storing match data. Good organization now means less pain later, like placing early wards before the gank arrives.
Create the Match Data Bucket
- Navigate to S3 - In AWS Console, search for "S3" in the top search bar
- Create bucket - Click "Create bucket"
- Bucket name - Enter something like
rift-rewind-match-data-[your-name]
- Example:
rift-rewind-match-data-alex - Bucket names must be globally unique across all AWS accounts
- Use lowercase letters, numbers, and hyphens only
- Keep default security - Ensure "Block all public access" is checked ✅
- All four checkboxes should be checked
- This keeps your data private (because nobody needs to see that 0/10 Yasuo game)
- Create bucket - Click "Create bucket" at the bottom
You'll know you're done when you see your new bucket in the list with a green "Successfully created bucket" message.
Note: We don't need to manually create folders inside the bucket - the Lambda function will automatically create the folder structure when it saves files.
Step 3: Store Your API Key Securely 🔐
Never hardcode API keys in your Lambda functions - that's like streaming with your password visible. Instead, we'll use AWS Systems Manager Parameter Store, which is basically a vault for your sensitive data.
Store the Riot API Key
- Navigate to Systems Manager - Search for "Systems Manager" in AWS Console
- Go to Parameter Store - In the left sidebar, click "Parameter Store"
- Create parameter - Click "Create parameter"
- Configure the parameter:
- Name:
/rift-rewind-challenge2/riot-api-key - Description: "Riot Games Development API key for match data collection"
- Tier: Standard
- Type: SecureString (this encrypts your key)
- Value: Paste your Riot API key here
- Create parameter - Click "Create parameter"
You'll know you're done when you see "Create parameter request succeeded" at the top of the page.
Note: Your API Key is set to expire every 48 hours, so please ensure the existing API Key is not expired, otherwise the retrieval will not work.
Why Parameter Store?
Parameter Store provides:
- Encryption - Your API key is encrypted, not sitting around in plain text
- Access control - Only services you authorize can retrieve it
- Version history - Track changes to your configuration
- No exposure - Keys never appear in your code or logs
It's like keeping your password in a password manager instead of a sticky note on your monitor.
Step 4: Understanding AWS Permissions 🤔
Before we set up Lambda, let's talk about how AWS handles permissions. This might seem like "boring security stuff," but understanding it will help you build more secure projects in the future.
The Principle of Least Privilege
In security, there's a concept called Principle of Least Privilege - it means giving an account or service only the minimum permissions it needs to do its job, nothing more. Think of it like champion abilities: you wouldn't give every champion a global ultimate, invulnerability, and infinite dashes. Each champion gets the specific tools they need for their role.
Why it matters:
- If something goes wrong (a bug, a hacked account, a mistake in your code), the damage is limited
- You can't accidentally delete things you shouldn't have access to
- It's easier to troubleshoot issues when permissions are specific
- It's a fundamental security practice used by every major tech company
Example: Your Lambda function needs to:
- Read one specific API key from Parameter Store
- Write files to one specific S3 bucket
- Write logs to CloudWatch
It doesn't need to:
- Delete S3 buckets
- Read all parameters in your account
- Modify IAM permissions
- Launch EC2 instances
In production applications, you'd create custom policies that grant exactly those three permissions and nothing else.
The Learning Approach vs. Production Approach
For this challenge, we're taking a simplified learning approach that grants broader permissions than strictly necessary. This is intentional - it reduces complexity so you can focus on building rather than fighting with permissions.
What we're doing: Using AWS managed policies that grant access to all S3 buckets and all Parameter Store parameters
What production would do: Create custom policies limiting access to only your specific bucket and specific parameter
The tradeoff: We're prioritizing learning speed over perfect security. For a personal learning project with no sensitive data, this is fine. For a real application handling user data or running in a company, you'd invest the extra time to implement least privilege properly.
Think of it like learning a new champion: you might start with a recommended build to understand the basics, then optimize your build once you know what you're doing.
Step 5: Create Lambda Execution Role 🛡️
Now let's set up the permissions your Lambda function needs. We're using the simplified approach with AWS managed policies.
Create the Role
- Go to IAM - Search for "IAM" in AWS Console
- Create role - Click "Roles" then "Create role"
- Configure role:
- Trusted entity type: AWS service
- Use case: Lambda (select it from the list)
- Click "Next"
- Add permissions - Search for and check the box next to each of these three policies:You can search for them one at a time in the search box, checking each one as you find it.
AWSLambdaBasicExecutionRole(lets Lambda write logs to CloudWatch)AmazonSSMReadOnlyAccess(lets Lambda read from Parameter Store)AmazonS3FullAccess(lets Lambda read/write to S3)
- Name the role:
- Click "Next"
- Role name:
rift-rewind-challenge2-lambda-role - Description: "Execution role for match data collection Lambda"
- Create role - Click "Create role"
About These Permissions
The three policies you just attached do the following:
- AWSLambdaBasicExecutionRole: Standard policy that every Lambda function needs - allows writing logs so you can debug issues
- AmazonSSMReadOnlyAccess: Grants read access to all Parameter Store parameters (in production, you'd limit this to just your specific parameter)
- AmazonS3FullAccess: Grants full access to all S3 buckets (in production, you'd limit this to just your specific bucket)
Security Note: These managed policies grant broader permissions than strictly necessary - they follow the "training wheels" approach for learning. For this challenge with no sensitive data and no other resources in your account, this simplified approach is fine and lets you focus on learning the core concepts. If you continue building AWS projects after this challenge, you'll want to learn how to create custom IAM policies that implement least privilege properly. AWS documentation has great tutorials on this, and the extra security is worth the effort for real applications.
Step 6: Write the Match Data Lambda Function ⚡
Time to write the Lambda function that fetches match data and saves it to S3. This is where the magic happens.
Create the Lambda Function
- Navigate to Lambda - Search for "Lambda" in AWS Console
- Create function:
- Click "Create function"
- Choose "Author from scratch"
- Function name:
fetch-match-history - Runtime: Python 3.13
- Architecture: Leave as default (x86_64)
- Execution role: Choose "Use an existing role"
- Select
rift-rewind-challenge2-lambda-rolefrom the dropdown - Click "Create function"
Add the Match Data Code
You'll see a code editor with some default Python code. Select all that code and delete it, then paste in the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
import json
import boto3
import urllib3
from urllib.parse import quote
from datetime import datetime
def lambda_handler(event, context):
"""
Fetches League of Legends match history for a summoner using Riot ID.
Expected event: {"riotId": "GameName#TAG", "region": "na1", "count": 5}
"""
# Configuration - UPDATE THIS WITH YOUR BUCKET NAME
bucket_name = 'YOUR_BUCKET_NAME_HERE' # Replace with your actual bucket name
try:
# Parse the event
riot_id = event.get('riotId', '').strip()
region = event.get('region', 'na1')
match_count = event.get('count', 5)
# Validate input
if not riot_id:
return {
'statusCode': 400,
'error': 'Riot ID is required'
}
# Validate Riot ID format
if '#' not in riot_id:
return {
'statusCode': 400,
'error': 'Please use Riot ID format: GameName#TAG (e.g., Hide on bush#KR1)'
}
# Get API key from Parameter Store
ssm = boto3.client('ssm')
try:
parameter = ssm.get_parameter(
Name='/rift-rewind-challenge2/riot-api-key',
WithDecryption=True
)
api_key = parameter['Parameter']['Value']
except Exception as e:
print(f"Failed to get API key: {str(e)}")
return {
'statusCode': 500,
'error': 'Failed to retrieve API key'
}
# Initialize HTTP client and S3
http = urllib3.PoolManager()
s3 = boto3.client('s3')
headers = {'X-Riot-Token': api_key}
# Step 1: Get account PUUID using Riot ID
game_name, tag_line = riot_id.split('#', 1)
game_name = quote(game_name)
tag_line = quote(tag_line)
routing_value = get_routing_value(region)
account_url = f"https://{routing_value}.api.riotgames.com/riot/account/v1/accounts/by-riot-id/{game_name}/{tag_line}"
print(f"Fetching account data for {riot_id}")
account_response = http.request('GET', account_url, headers=headers)
if account_response.status == 404:
return {
'statusCode': 404,
'error': 'Riot ID not found. Check spelling and region.'
}
elif account_response.status == 403:
return {
'statusCode': 403,
'error': 'Your API key has expired. Please regenerate it in the Riot Developer Portal.'
}
elif account_response.status != 200:
return {
'statusCode': account_response.status,
'error': f'Failed to fetch account: {account_response.status}'
}
account_data = json.loads(account_response.data.decode('utf-8'))
puuid = account_data['puuid']
summoner_name = f"{account_data['gameName']}#{account_data['tagLine']}"
# Step 2: Get match list
match_list_url = f"https://{routing_value}.api.riotgames.com/lol/match/v5/matches/by-puuid/{puuid}/ids?start=0&count={match_count}"
print(f"Fetching match list for {summoner_name}")
match_list_response = http.request('GET', match_list_url, headers=headers)
if match_list_response.status != 200:
return {
'statusCode': match_list_response.status,
'error': f'Failed to fetch match list: {match_list_response.status}'
}
match_ids = json.loads(match_list_response.data.decode('utf-8'))
if not match_ids:
return {
'statusCode': 404,
'error': 'No matches found for this summoner'
}
# Step 3: Fetch and process each match
processed_matches = []
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
for i, match_id in enumerate(match_ids):
print(f"Processing match {i+1}/{len(match_ids)}: {match_id}")
# Get full match data
match_url = f"https://{routing_value}.api.riotgames.com/lol/match/v5/matches/{match_id}"
match_response = http.request('GET', match_url, headers=headers)
if match_response.status != 200:
print(f"Failed to fetch match {match_id}: {match_response.status}")
continue
match_data = json.loads(match_response.data.decode('utf-8'))
# Save full match data to S3
full_key = f"match-history/{summoner_name}/full/{match_id}_{timestamp}.json"
s3.put_object(
Bucket=bucket_name,
Key=full_key,
Body=json.dumps(match_data, indent=2),
ContentType='application/json'
)
# Extract player stats
player_stats = extract_player_stats(match_data, puuid)
if player_stats:
# Save extracted stats to S3
stats_key = f"match-history/{summoner_name}/stats/{match_id}_{timestamp}.json"
s3.put_object(
Bucket=bucket_name,
Key=stats_key,
Body=json.dumps(player_stats, indent=2),
ContentType='application/json'
)
processed_matches.append({
'matchId': match_id,
'champion': player_stats.get('championName'),
'kda': f"{player_stats.get('kills', 0)}/{player_stats.get('deaths', 0)}/{player_stats.get('assists', 0)}",
'win': player_stats.get('win'),
'fullDataLocation': full_key,
'statsLocation': stats_key
})
return {
'statusCode': 200,
'summoner': summoner_name,
'region': region,
'matchesProcessed': len(processed_matches),
'matches': processed_matches,
'message': f'Successfully processed {len(processed_matches)} matches for {summoner_name}'
}
except Exception as e:
print(f"Error: {str(e)}")
import traceback
traceback.print_exc()
return {
'statusCode': 500,
'error': 'Internal server error'
}
def extract_player_stats(match_data, puuid):
"""Extract relevant player statistics from match data"""
try:
# Find the participant data for our player
participants = match_data['info']['participants']
player_data = None
for participant in participants:
if participant['puuid'] == puuid:
player_data = participant
break
if not player_data:
return None
# Extract key statistics
stats = {
'matchId': match_data['metadata']['matchId'],
'gameCreation': match_data['info']['gameCreation'],
'gameDuration': match_data['info']['gameDuration'],
'gameMode': match_data['info']['gameMode'],
'queueId': match_data['info']['queueId'],
'championName': player_data['championName'],
'championId': player_data['championId'],
'teamPosition': player_data['teamPosition'],
'individualPosition': player_data['individualPosition'],
'kills': player_data['kills'],
'deaths': player_data['deaths'],
'assists': player_data['assists'],
'totalMinionsKilled': player_data['totalMinionsKilled'],
'neutralMinionsKilled': player_data['neutralMinionsKilled'],
'goldEarned': player_data['goldEarned'],
'totalDamageDealtToChampions': player_data['totalDamageDealtToChampions'],
'totalDamageTaken': player_data['totalDamageTaken'],
'visionScore': player_data['visionScore'],
'win': player_data['win'],
'items': [
player_data['item0'],
player_data['item1'],
player_data['item2'],
player_data['item3'],
player_data['item4'],
player_data['item5'],
player_data['item6'] # trinket
],
'summoner1Id': player_data['summoner1Id'],
'summoner2Id': player_data['summoner2Id'],
'perks': {
'primaryStyle': player_data['perks']['styles'][0]['style'],
'subStyle': player_data['perks']['styles'][1]['style'],
'primaryPerk': player_data['perks']['styles'][0]['selections'][0]['perk']
}
}
return stats
except Exception as e:
print(f"Error extracting player stats: {str(e)}")
return None
def get_routing_value(region):
"""Map platform region to routing value for Riot API"""
routing_map = {
'na1': 'americas',
'br1': 'americas',
'la1': 'americas',
'la2': 'americas',
'euw1': 'europe',
'eun1': 'europe',
'tr1': 'europe',
'ru': 'europe',
'kr': 'asia',
'jp1': 'asia',
'oc1': 'sea',
'ph2': 'sea',
'sg2': 'sea',
'th2': 'sea',
'tw2': 'sea',
'vn2': 'sea'
}
return routing_map.get(region, 'americas')Critical step: Find the line that says
bucket_name = 'YOUR_BUCKET_NAME_HERE' (it's near the top of the code, around line 14) and replace YOUR_BUCKET_NAME_HERE with your actual bucket name from Step 2.For example, change:
YOUR_BUCKET_NAME_HERETo:
rift-rewind-match-data-alexDeploy the Function
After pasting the code and updating your bucket name:
- Click the Deploy button near the top of the code editor
- Wait a few seconds for the "Changes deployed" success message to appear
Your function is now ready to run.
Step 7: Test Your Data Pipeline 🧪
Time to see if everything works. We're going full custom game mode here - testing before taking it live.
Configure a Test Event
- Go to Test tab - Click the "Test" tab at the top of your Lambda function page
- Create test event:
- Click the orange "Test" button
- In the dropdown that appears, select "Configure test event"
- Event name:
TestMatchFetch - Replace the default JSON with (Refer to example test case below for example inputs):
{ "riotId": "", "region": "", "count":}
Important: Replace the Riot ID with a real one. You can use your own (find it in your League client profile), or use a pro player's:
Hide on bush#KR1(Faker, Korea)Doublelift#NA1(North America)Rekkles#EUW(EU West)
Example Test Case:
{"riotId": "Hide on bush#KR1", "region": "kr", "count": 3}Note: In this example, the “count” is used to represent the number of matches to retrieve.
Make sure the region code matches the account's region.
- Save - Click "Save"
Run the Test
- Test - Click the orange "Test" button
- Wait - The function takes 10-30 seconds to run (fetching data from Riot's API takes time, just like loading into a game)
- Check results - Look at the "Execution result" section:
- Should say "succeeded" in green
- The response will show JSON with your matches, including champion names, KDA, and file locations
- Logs show detailed execution steps
If you see red and "failed", don't panic - check the Troubleshooting section below.
Verify Data in S3
- Go to S3 - Navigate back to your S3 bucket
- Check folders - Click into
match-history/then the summoner name folder. You should see two subfolders:
full/- Complete match data from Riot API (the whole enchilada)stats/- Extracted player stats - champion, items, KDA, etc. (the highlight reel)
- View files - Click through to see JSON files with timestamps in the names
- Download a stats file - Click on a file in the
stats/folder, then click "Download". Open it in a text editor to see the cleaned data structure with all your match details - Compare formats - Download a file from
full/too. Notice how the stats files are way smaller and easier to read - we've done the hard work of extracting what matters.
Troubleshooting Common Issues
"Please use Riot ID format" error:
- Make sure you're using the GameName#TAG format (with the # symbol)
- Example:
PlayerName#NA1, not justPlayerName
"Riot ID not found" error:
- Double-check the Riot ID spelling (it's case-sensitive)
- Verify the TAG matches the account (NA1, EUW, KR1, etc.)
- Make sure the region code matches where the account is located
"Your API key has expired" (403 status):
- Go back to the Riot Developer Portal
- Click "REGENERATE API KEY"
- Update the Parameter Store with the new key
- Wait a minute, then try again
"Access Denied" or permission errors:
- Verify your Lambda role has all three policies attached (
AWSLambdaBasicExecutionRole,AmazonSSMReadOnlyAccess,AmazonS3FullAccess) - Check the Parameter Store parameter name is exactly
/rift-rewind-challenge2/riot-api-key - Make sure your bucket name in the code matches your actual bucket
Rate limit errors (429 status):
- Development keys have strict limits (20/sec, 100/2min)
- Wait a minute and try again
- Reduce the match count in your test event to 2-3
"No matches found" error:
- The account might not have any recent matches
- Try a different Riot ID with known recent games
- Make sure it's a League of Legends account (not Valorant or TFT)
Nothing seems to work:
- Click "Monitor" then "View CloudWatch logs" to see detailed error messages
- CloudWatch logs are your replay system - they show exactly what went wrong and where
- Look for the latest log stream and check the error messages
Step 8: Understanding Data Size and Costs 💰
Let's talk about what you're storing and whether this will drain your credits faster than a tilted losing streak.
Data Size Estimates
- Single match (full data): 50-150 KB per match
- Single match (stats only): 2-5 KB per match
- 10 matches: Around 500 KB - 1.5 MB total
- 100 matches: Around 5-15 MB total
- 1,000 matches: Around 50-150 MB total
You're nowhere near the 2GB danger zone. You could store thousands of matches before hitting any real limits. For perspective, your entire ranked season history would probably fit in under 50 MB.
S3 Storage Costs
For this project in US East region:
- Storage: $0.023 per GB per month (pennies)
- PUT requests: $0.005 per 1,000 requests (basically free)
- GET requests: $0.0004 per 1,000 requests (even more basically free)
Real-world example: Storing 1,000 matches (about 100 MB):
- Storage cost: About $0.0023 per month (yes, that's a fraction of a penny)
- 2,000 PUT requests for full + stats files: About $0.01 one-time
- Total: Less than 2 cents per month
Lambda Costs
- Requests: $0.20 per 1 million requests
- Duration: $0.00001667 per GB-second of compute time
Real-world example: Running your function 100 times:
- Request charges: About $0.00002
- Duration charges (30 seconds each at 128 MB): About $0.0006
- Total: Less than a penny for 100 runs
Your Free Tier credits ($100+ from account creation and activities) easily cover this. You'd need to run this thousands of times to even make a dent.
Checklist ✅
Mark off each item as you complete it:
- [ ] Created Riot Developer account and got API key
- [ ] Stored API key securely in Parameter Store
- [ ] Created S3 bucket for match data
- [ ] Created Lambda execution role with managed policies
- [ ] Deployed Lambda function with match fetching code
- [ ] Updated bucket name in the code
- [ ] Tested function successfully with a Riot ID
- [ ] Verified match data appears in S3 (both full and stats folders)
- [ ] Downloaded and checked a stats file to see champion and item data
Next Steps 🚀
You've built a data collection pipeline - here's what you've accomplished:
✅ Secure credential management with Parameter Store
✅ Serverless data fetching with Lambda
✅ Cloud storage with S3
✅ Real API integration with rate limiting
✅ Organized data structure with extracted stats
✅ Champion, item, and performance data ready for analysis
This is the foundation of any data-driven League application. Your raw match data is now stored in S3, along with extracted player statistics including champion picks, item builds, KDA, CS, gold, and damage metrics.
Tomorrow on Day 2, you'll learn how to clean and aggregate this match data using SageMaker Data Wrangler. We'll transform these individual match stats into actually useful insights - things like champion win rates, average performance by role, which item builds are working, and whether you really do int on Yasuo as much as your teammates claim.
Pro tip: The pattern you learned today (secure credentials → serverless function → cloud storage) is used by data engineers at companies building the tools you use every day. You've just implemented production-ready architecture that scales from hobby projects to enterprise applications.
Now go test it with your duo's Riot ID and see if the data backs up your theory about who's really carrying.
GG on building your first data pipeline! 🎮📊
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article