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.
What is a sorted set?
A sorted set is a key-value data structure where each member has two components:
- A score (float64) – used for ordering
- A value (string) – the actual data
Here’s a simple example:
|
1 2 3 |
ZADD myleaderboard 10000 bob ZADD myleaderboard 5000 jane ZADD myleaderboard 11000 peter |
Properties
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.
The classic leaderboards use case
Let’s start with the example that sorted sets are known for. Here’s how you’d implement a real-time gaming leaderboard:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# Add/update score ZADD game:scores 5000 player1 # Increment score when player completes a game ZINCRBY game:scores 100 player1 # Get top 10 players with scores ZREVRANGE game:scores 0 9 WITHSCORES # Get a player's rank ZREVRANK game:scores player1 # Get a player's score ZSCORE game:scores player1 |
Why are sorted sets so good for leaderboards?
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:
- Scan through all rows
- Sort them (expensive!)
- Return the top 10
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.
Beyond leaderboards
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.
1. Time-based task queue
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import time import valkey # Schedule tasks with unix timestamp as the score execute_at = time.time() + 300 # 5 minutes from now valkey.zadd('task:queue', {task_id: execute_at}) # Worker retrieves tasks due for execution def process_due_tasks(): now = time.time() tasks = valkey.zrangebyscore('task:queue', 0, now) for task in tasks: execute_task(task) 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.
2. Rate limiting with sliding window
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
def check_rate_limit(user_id, limit=100, window=60): """Allow 'limit' requests per 'window' seconds""" key = f'ratelimit:{user_id}' now = time.time() cutoff = now - window # Remove old requests outside the window valkey.zremrangebyscore(key, 0, cutoff) # Count current requests in window count = valkey.zcard(key) if count < limit: valkey.zadd(key, {str(now): now}) valkey.expire(key, window + 1) # Cleanup key return True 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.
3. Autocomplete with prefix search
Here’s one you may not know about; build your own autocomplete using lexicographical ordering:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# Add terms with score 0 (lexicographical ordering) valkey.zadd('autocomplete', { 'apple': 0, 'application': 0, 'apply': 0, 'banana': 0, 'band': 0, 'bandana': 0 }) def autocomplete(prefix, count=10): # ZRANGEBYLEX requires bracket notation start = f'[{prefix}' end = f'[{prefix}xff' # xff is max char return valkey.zrangebylex('autocomplete', start, end, 0, count) # Examples autocomplete('app') # ['apple', 'application', 'apply'] 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.
4. Trending topics with time decay
Implement social media-style trending algorithms:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import math def calculate_trending_score(upvotes, hours_old, gravity=1.8): """Hacker News ranking algorithm""" return (upvotes - 1) / math.pow(hours_old + 2, gravity) def add_trending_post(post_id, upvotes, created_at): hours_old = (time.time() - created_at) / 3600 score = calculate_trending_score(upvotes, hours_old) valkey.zadd('trending', {post_id: score}) def get_trending_posts(count=20): return valkey.zrevrange('trending', 0, count - 1) # Periodically recalculate scores for decay # 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.
5. Range queries for product filtering
Implement multi-attribute filtering similar to SQL WHERE clauses:
|
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 |
# Multiple sorted sets for different attributes valkey.zadd('products:price', { 'laptop_001': 999.99, 'laptop_002': 1299.99, 'laptop_003': 799.99 }) valkey.zadd('products:rating', { 'laptop_001': 4.5, 'laptop_002': 4.8, 'laptop_003': 4.2 }) # Find products in price range valkey.zrangestore( 'price_range', # Destination key 'products:price', # Source key 800, 1200, # Min and Max byscore=True # Use scores not ranks ) # Find products in rating range valkey.zrangestore( 'rating_range', 'products:rating', 4.0, 5.0, byscore=True ) # Combine filters with ZINTERSTORE 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.
Performance
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.
Best Practices
1. Memory optimization
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.
2. Use pipelining
When adding multiple elements, batch your ZADD operations using pipelining to reduce round trips:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
# Good - single round trip pipe = valkey.pipeline() for item in items: pipe.zadd('myset', {item.name: item.score}) pipe.execute() # Bad - multiple round trips for item in items: valkey.zadd('myset', {item.name: item.score}) # Separate network call |
3. Cleanup old data
Use ZREMRANGEBYSCORE or EXPIRE to prevent sorted sets from growing unbounded:
|
1 2 3 4 5 6 |
# Remove entries older than 24 hours in a time-based set cutoff = time.time() - 86400 valkey.zremrangebyscore('timeseries', 0, cutoff) # Or expire the entire key after a period valkey.expire('rate:limit:user123', 3600) |
4. Combine multiple sets
Don’t feel limited to a single monolithic sorted set. Use ZUNIONSTORE and ZINTERSTORE to combine multiple sets for complex queries.
Recent performance improvements
The Valkey community has invested heavily in optimizing sorted sets. Some recent highlights:
Valkey 8.1
- 27% less memory usage compared to Valkey 8.0
- 28% less memory usage compared to Redis 8.2
- ZRANK operations 45% faster using SIMD instructions
Future improvements being explored:
- Embedding elements directly in skiplist nodes (reduces pointer overhead)
- Optimizing ZRANK to avoid path comparisons
- Replacing skiplist with B+ tree variant for certain workloads
These improvements are automatic—just upgrade Valkey and you get faster, more memory-efficient sorted sets for free.
Takeaways
Sorted sets aren’t just for leaderboards. The combination of a score and value enables some other great use cases:
- Time-based task queues with ZRANGEBYSCORE
- Rate limiting with sliding windows
- Autocomplete with prefix search using lexicographical ordering
- Trending topics with time decay algorithms
- Range queries on any numeric attribute
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.
Try it
Ready to experiment with sorted sets? Here’s how to get started:
- Try it out: github.com/valkey-io/valkey
- Join the community: valkey.io/community/
- Join the conversation: Valkey Slack
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.
Valkey and Redis at Percona
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.