CloudKit: Efficient way to get user's rank in leaderboard without fetching all records?
I'm building a leaderboard feature using CloudKit's public database and need advice on the best approach to calculate a user's rank efficiently.
Current Setup
Record Structure:
- Record Type:
LeaderboardScore - Fields:
period(String): "daily", "weekly", "monthly", "allTime"score(Int): User's scoreprofile(Reference): Link to user's profileachievedAt(Date): Timestamp
Leaderboard Display:
- Initially fetch first 15 users (sorted by score descending)
- Paginate to load more as user scrolls
- Show total player count
- Show current user's rank (even if not in top 15)
The Challenge
I can fetch the first 15 users easily with a sorted query, but I need to display the current user's rank regardless of their position. For example:
- User could be ranked #1 (in top 15) ✅ Easy
- User could be ranked #247 (not in top 15) ❌ How to get this efficiently?
My Current Approach
Query records with scores higher than the user's score and count them:
// Count how many users scored higher
let predicate = NSPredicate(
format: "period == %@ AND score > %d",
period, userScore
)
// Rank = count + 1
Concerns
- For 1000+ users with better scores, this requires multiple paginated queries
- Even with
desiredKeys: [], I'm concerned about performance and CloudKit request limits
Questions
-
Is there a CloudKit API I'm missing that can efficiently count records matching a predicate without fetching all the records and paginating?
-
Is this approach acceptable for a leaderboard with 1K-10K users? Does fetching with
desiredKeys: []help significantly with performance? -
Are there any optimizations I should consider to make this more efficient?
-
What's the recommended approach for calculating user rank in CloudKit at this scale?
Current Scale
- Expected: 1,000-10,000 active users per leaderboard period
- Platform: iOS 17+, SwiftUI
Any guidance on best practices for leaderboards usecase in CloudKit would be greatly appreciated!
Oh, I thought that the score was the rank. Now that I understand it is actually the order in the result set sorted by score. Thanks for your elaboration.
In that case, you can probably try CKQuerySubscription, which allows you to observe the changes on LeaderboardScore and implement the following flow:
-
Set up a
CKQuerySubscriptionto get notified of any changes onLeaderboardScore. Use the desiredKeys property ofCKSubscription.NotificationInfoto specify the fields to include in the notification’s payload, which will bescoreanduserIDhere. -
Do an initial fetch to retrieve all the users and their scores you currently have.
-
When getting a notification of the subscription, grab the
scoreanduserIDfrom the payload and update your local cache.
The only thing I am not quite sure about CKQuerySubscription is if the system always delivers the notifications to all users (other than the user who made the change) successfully. I think only real-world testing can tell – If you get a chance to try, please share your observation.
Other than that, I don't see anything better than your current implementation, which queries records with scores higher than the current user's score and counts them, with desiredKeys being set to [].
Best,
——
Ziqiao Chen
Worldwide Developer Relations.