A real-time, multi-period leaderboard API built entirely on managed AWS services — API Gateway, Lambda, and DynamoDB — with an EC2/Nginx dashboard on the front end. No Redis, no sorted-set hacks, no servers to patch: DynamoDB's own index does the sorting.
Players → EC2/Nginx dashboard → API Gateway → 5 Lambda functions → DynamoDB (us-east-1)
| File | Route | Job |
|---|---|---|
lambda/score-submit.py |
POST /scores |
Writes a score into all-time, daily-*, and weekly-* in one call, keeping the higher of the new and existing score for each ("high-score-wins"). |
lambda/leaderboard-query.py |
GET /leaderboard |
Queries the rank GSI for a period (?period=all-time|daily|weekly) and returns the top N, already sorted — no app-side sort. |
lambda/player-stats.py |
GET /player |
Looks up one player's entries across every leaderboard they're on and computes live rank + percentile for each. |
lambda/leaderboard-snapshot.py |
POST /snapshot |
Freezes the current top-N of a leaderboard into a history table, timestamped, for later trend/audit views. |
lambda/score-simulator.py |
POST /simulate |
Batch-generates realistic players and scores across all three periods — the seed data behind the screenshots below. |
All five functions run under one shared IAM execution role scoped to the two DynamoDB tables and CloudWatch Logs.
DynamoDB's GSI sort key only reads efficiently in one direction (ascending). Rather than scan-and-sort in application code, the score is inverted at write time so an ascending index query returns players in descending rank order for free:
MAX_SCORE = 999999
SCORE_PAD = 7
def make_inverted(score, player_id):
"""Convert real score to inverted sort key for descending order in GSI."""
inv = MAX_SCORE - int(score)
return f'{str(inv).zfill(SCORE_PAD)}#{player_id}'| Player | Real score | Stored inverted_score |
|---|---|---|
| CosmicWolf | 9,011 | 0990988#cosmicwolf |
| BlazeMaster | 8,851 | 0991148#blazemaster |
| EpsilonEdge | 8,357 | 0991642#epsilonedge |
Zero-padding to 7 digits keeps the string comparison numerically correct; appending player_id guarantees a unique, tie-broken key even when two players finish with identical scores. leaderboard-query.py then just does:
table.query(
IndexName='leaderboard-rank-index',
KeyConditionExpression=Key('leaderboard_id').eq(leaderboard_id),
ScanIndexForward=True, # ascending inverted = descending real score
Limit=limit,
)A single submission fans out to every active leaderboard period, but only ever raises a score:
for lb_id in get_leaderboard_ids(): # all-time, daily-*, weekly-*
existing = table.get_item(Key={'player_id': player_id, 'leaderboard_id': lb_id}).get('Item')
current = int(existing.get('score', 0)) if existing else 0
new_score = max(int(score), current) # high-score-wins
table.put_item(Item={..., 'score': new_score, 'inverted_score': make_inverted(new_score, player_id)})No stored rank column to keep in sync — a player's position is derived on read by counting how many inverted scores beat theirs:
rank_resp = table.query(
IndexName='leaderboard-rank-index',
KeyConditionExpression=Key('leaderboard_id').eq(lb_id) & Key('inverted_score').lt(inv_score),
Select='COUNT',
)
rank = rank_resp.get('Count', 0) + 1
percentile = round(((total - rank) / total) * 100, 1)leaderboard-scores — the live, mutable state
- PK
player_id, SKleaderboard_id - GSI
leaderboard-rank-indexonleaderboard_id+inverted_score
leaderboard-snapshots — immutable, append-only history
- Keyed by
snapshot_id={leaderboard_id}#{timestamp}
38 players, top 25 returned in 293ms via the rank GSI — All-Time / Today / This Week tabs, live query timing, and one-click snapshot/seed controls for demos.
All five functions deployed and independently scalable (Python 3.14, zip package).
- Inverted-score GSI — descending rank from an ascending index, no Redis sorted set needed
- High-score-wins writes — read-then-conditional-max on every submit protects existing bests
- Multi-period fan-out — one submission updates all-time, daily, and weekly leaderboards per-item
- Derived percentile — rank and percentile computed on read via
COUNTqueries, never stored or allowed to drift - Point-in-time snapshots — historical leaderboard states captured on demand into an append-only table
- CORS-enabled REST API — every Lambda handles its own
OPTIONSpreflight for the browser dashboard
Gaming leaderboards & tournaments · sales performance tracking · educational quiz platforms · fitness & health rankings · employee performance dashboards · social engagement metrics — anywhere you need a ranked, high-score-wins view over a large, frequently-updated set of scores.
API Gateway (REST, 5 routes) · Lambda (Python 3.14, 5 functions) · DynamoDB (2 tables) · EC2 + Nginx (dashboard) · IAM (single shared execution role) · CloudWatch (logs)


