This blog post covers the details about sorted set use cases as discussed in this video. Sorted sets are one of the most powerful data structures in Valkey and Redis. While most developers immediately think about “gaming leaderboards” when they hear about sorted sets, this versatile data type can solve many problems, from task scheduling to autocomplete to rate limiting.
A sorted set is a key-value data structure where each member has two components:
Here’s a simple example:
|
1 |
ZADD myleaderboard 10000 bob <br>ZADD myleaderboard 5000 jane <br>ZADD myleaderboard 11000 peter |
O(log N) performance: insertion and retrieval time don’t increase much as the set grows. This logarithmic complexity means sorted sets remain fast even with thousands of elements.
Always sorted: unlike relational databases, where you’d need an expensive ORDER BY operation to rank rows, sorted sets store elements in sorted order by score. This means retrieving the top 10 items is instant, as there isn’t any sorting done.
Unique members: each member can appear only once in a set. Exactly what you want in a leaderboard where the members are player names.
Let’s start with the example that sorted sets are known for. Here’s how you’d implement a real-time gaming leaderboard:
|
1 |
# Add/update score<br>ZADD game:scores 5000 player1<br><br># Increment score when player completes a game<br>ZINCRBY game:scores 100 player1<br><br># Get top 10 players with scores<br>ZREVRANGE game:scores 0 9 WITHSCORES<br><br># Get a player's rank<br>ZREVRANK game:scores player1<br><br># Get a player's score <br>ZSCORE game:scores player1 |
Compare this to a relational database approach. In a traditional database, you’d have a table with rows for each player and their scores. To get the top 10, you’d need to:
With sorted sets, the data is already sorted. You simply fetch the range you need, whether it’s the top 10, bottom 10, or ranks 50-60. No scanning, no sorting overhead.
The scores update atomically thanks to Valkey’s single main execution loop for writes, and you can retrieve any range without scanning the whole set. Plus, ZREVRANK gives you a player’s ranking instantly, again without any sorting operation.
Now for the interesting part. The sorted set’s combination of a score and value opens up possibilities far beyond gaming. Let’s explore some of these creative applications.
The LIST datatype in Valkey is great for queuing, but what if you need to schedule jobs for future execution? Use Unix timestamps as scores with the sorted set:
|
1 |
import time<br>import valkey<br><br># Schedule tasks with unix timestamp as the score<br>execute_at = time.time() + 300 # 5 minutes from now<br>valkey.zadd('task:queue', {task_id: execute_at})<br><br># Worker retrieves tasks due for execution<br>def process_due_tasks():<br> now = time.time()<br> tasks = valkey.zrangebyscore('task:queue', 0, now)<br><br> for task in tasks:<br> execute_task(task)<br> valkey.zrem('task:queue', task) |
Use cases: Delayed job execution, scheduled reminders, deferred email sending, and appointment notifications.
The beauty here is that ZRANGEBYSCORE efficiently retrieves only the tasks due for execution. No need to scan through thousands of future tasks—the sorted structure lets you grab exactly what you need.
Counters can be used for rate limiting, increasing the count when a request is made (if under the limit) and decreasing when completed. A more sophisticated rate limiting would use the sorted set with the timestamps as scores:
|
1 |
def check_rate_limit(user_id, limit=100, window=60):<br> """Allow 'limit' requests per 'window' seconds"""<br><br> key = f'ratelimit:{user_id}'<br> now = time.time()<br> cutoff = now - window<br> <br> # Remove old requests outside the window<br> valkey.zremrangebyscore(key, 0, cutoff)<br> <br> # Count current requests in window<br> count = valkey.zcard(key)<br> <br> if count < limit:<br> valkey.zadd(key, {str(now): now})<br> valkey.expire(key, window + 1) # Cleanup key<br> return True<br> <br> return False # Rate limited |
Use cases: API rate limiting, DDoS protection, request throttling, login attempt limiting.
This sliding window approach is more sophisticated than simple counters because it tracks exactly when each request occurred. The ZREMRANGEBYSCORE command efficiently removes old entries, and ZCARD gives you the count instantly.
Here’s one you may not know about; build your own autocomplete using lexicographical ordering:
|
1 |
# Add terms with score 0 (lexicographical ordering)<br>valkey.zadd('autocomplete', {<br> 'apple': 0, 'application': 0, 'apply': 0,<br> 'banana': 0, 'band': 0, 'bandana': 0<br>})<br><br>def autocomplete(prefix, count=10):<br> # ZRANGEBYLEX requires bracket notation<br> start = f'[{prefix}'<br> end = f'[{prefix}xff' # xff is max char<br><br> return valkey.zrangebylex('autocomplete', start, end, 0, count)<br><br># Examples<br>autocomplete('app') # ['apple', 'application', 'apply']<br>autocomplete('band') # ['band', 'bandana'] |
Use cases: Search suggestions, command completion, tag lookups, dictionary searches.
When all scores are zero, sorted sets fall back to lexicographical (alphabetical) ordering. The ZRANGEBYLEX command then lets you efficiently find all entries matching a prefix. It’s a simple but effective autocomplete solution.
Implement social media-style trending algorithms:
|
1 |
import math<br><br>def calculate_trending_score(upvotes, hours_old, gravity=1.8):<br> """Hacker News ranking algorithm"""<br> return (upvotes - 1) / math.pow(hours_old + 2, gravity)<br><br>def add_trending_post(post_id, upvotes, created_at):<br> hours_old = (time.time() - created_at) / 3600<br> score = calculate_trending_score(upvotes, hours_old)<br> valkey.zadd('trending', {post_id: score})<br><br>def get_trending_posts(count=20):<br> return valkey.zrevrange('trending', 0, count - 1)<br><br># Periodically recalculate scores for decay<br># Or use ZUNIONSTORE with weighted recent activity |
Use cases: Social media trends, hot topics detection, viral content tracking, and news feed ranking.
The score here represents how “hot” a topic is based on both popularity and recency. You’ll need to periodically recalculate scores as content ages, but retrieving the current top trending items is always instant.
Implement multi-attribute filtering similar to SQL WHERE clauses:
|
1 |
# Multiple sorted sets for different attributes<br><br>valkey.zadd('products:price', {<br> 'laptop_001': 999.99,<br> 'laptop_002': 1299.99,<br> 'laptop_003': 799.99<br>})<br><br>valkey.zadd('products:rating', {<br> 'laptop_001': 4.5,<br> 'laptop_002': 4.8,<br> 'laptop_003': 4.2<br>})<br><br># Find products in price range<br>valkey.zrangestore(<br> 'price_range', # Destination key<br> 'products:price', # Source key<br> 800, 1200, # Min and Max<br> byscore=True # Use scores not ranks<br>)<br><br><br># Find products in rating range<br>valkey.zrangestore(<br> 'rating_range',<br> 'products:rating',<br> 4.0, 5.0,<br> byscore=True<br>)<br><br># Combine filters with ZINTERSTORE<br>valkey.zinterstore('filtered', ['price_range', 'rating_range']) |
Use cases: E-commerce product filtering, user search with multiple criteria, and data segmentation.
This pattern lets you create separate sorted sets for each filterable attribute (price, rating, etc.), then combine them using set operations. ZINTERSTORE finds the intersection, products that match ALL criteria. You could also use ZUNIONSTORE for OR logic.
Understanding the time complexity helps you use sorted sets effectively:
| Command | Time Complexity | Notes |
| ZADD | O(log N) | Fast insertion |
| ZREM | O(log N) | Fast removal |
| ZSCORE | O(1) | Instant score lookup |
| ZRANK/ZREVRANK | O(log N) | Fast rank lookup |
| ZRANGE | O(log N+M) | M = elements returned |
| ZRANGEBYSCORE | O(log N+M) | M = elements in range |
| ZCARD | O(1) | Instant count |
| ZCOUNT | O(log N) | Fast range counting |
Note: ZRANGE and ZRANGEBYSCORE include M in their complexity. If you’re fetching 10 elements from a million-element set, you only pay for the 10 you retrieve, not the million you don’t need.
Sorted sets use memory-optimized storage for small sets (fewer than 128 entries with values under 64 bytes each). The configuration parameters zset-max-ziplist-entries and zset-max-ziplist-value control when this optimization kicks in.
When adding multiple elements, batch your ZADD operations using pipelining to reduce round trips:
|
1 |
# Good - single round trip <br><br>pipe = valkey.pipeline()<br>for item in items:<br> pipe.zadd('myset', {item.name: item.score})<br>pipe.execute()<br><br><br># Bad - multiple round trips <br><br>for item in items:<br> valkey.zadd('myset', {item.name: item.score}) # Separate network call |
Use ZREMRANGEBYSCORE or EXPIRE to prevent sorted sets from growing unbounded:
|
1 |
# Remove entries older than 24 hours in a time-based set <br>cutoff = time.time() - 86400<br>valkey.zremrangebyscore('timeseries', 0, cutoff)<br><br># Or expire the entire key after a period <br>valkey.expire('rate:limit:user123', 3600) |
Don’t feel limited to a single monolithic sorted set. Use ZUNIONSTORE and ZINTERSTORE to combine multiple sets for complex queries.
The Valkey community has invested heavily in optimizing sorted sets. Some recent highlights:
Valkey 8.1
Future improvements being explored:
These improvements are automatic—just upgrade Valkey and you get faster, more memory-efficient sorted sets for free.
Sorted sets aren’t just for leaderboards. The combination of a score and value enables some other great use cases:
It’s a core data type included in both Valkey and Redis with a straightforward API that’s easy to learn and powerful to use. Performance is excellent and continues to improve. The Valkey community is actively optimizing sorted sets, delivering automatic performance gains with each release. Think creatively about the score. It doesn’t have to represent a traditional “score” in a leaderboard; it can be a timestamp, a priority level, a price, a distance, or any numeric value that defines your ordering.
Ready to experiment with sorted sets? Here’s how to get started:
If you’ve built something interesting with sorted sets or have discovered another creative use case, I’d love to hear about it. The Valkey community is always excited to see what people build with these versatile data structures.
At Percona, we’re happy to support Open Source Redis and Valkey with enterprise-grade, 24/7 support and consultative expertise. Percona specialises in unbiased architectural guidance and seamless migrations, ensuring users can scale their data environments without being tied to proprietary licenses. More info.
Look out for our upcoming video and blog post on rate limiting strategies.
Resources
RELATED POSTS
comment tst
Looks good thanks for the test