Null Rank when retrieving Owner Records from expired Leaderboards

I’m aware of the issue retrieving null Leaderboard ranks that is present in 3.21.1, but I’ve tried downgrading to both 3.20.1 and 3.19.0 and I’m still seeing a similar issue when retrieving Records from already-expired Leaderboards.

If I call ListLeaderboardRecordsAsync and pass it the previous expiry timestamp of my Leaderboard, I get back both the Records and OwnerRecords from the previous iteration of the Leaderboard, which is the correct behavior. However, even though the Rank value is present in the Records, the OwnerRecords have null Ranks. This happens both in 3.20.1 and 3.19.0 and is reproducible both in the Unity Client and by using the API Explorer in the Nakama Console.

Versions: Nakama 3.20.1, 3.19.0 (with CockroachDB, in Docker, on Debian 12), Unity SDK 3.12.0
Server Framework Runtime language: Go

Expired leaderboards are evicted from the leaderboard rank cache, both for performance and memory usage optimization.

Since after expiration the ranks cannot change anymore, you could register a leaderboardReset hook that stores the current rank in the metadata of the record using a custom query, something like:

function storeLeaderboardRecordsRankInMetadata(logger: nkruntime.Logger, nk: nkruntime.Nakama, records: nkruntime.LeaderboardRecord[]) {
    let leaderboardId: string[] = [];
    let expiryTime: number[] = [];
    let ownerId: string[] = [];
    let rank: number[] = [];
    records.forEach(r => {
        expiryTime.push(r.expiryTime ? r.expiryTime : 0);

    const query = `
    WITH ranks AS (
        SELECT leaderboard_id, expiry_time, owner_id, rank
        FROM (SELECT unnest($1::text[]) leaderboard_id, unnest((SELECT array_agg(to_timestamp(n)) FROM unnest($2::int[]) n)) expiry_time, unnest($3::uuid[]) owner_id, unnest($4::int[]) rank) s1
        NATURAL JOIN leaderboard_record AS lr
    UPDATE leaderboard_record AS lr SET metadata = metadata || jsonb_build_object('rank', to_jsonb(ranks.rank::int))
    FROM ranks
    WHERE lr.leaderboard_id = ranks.leaderboard_id AND lr.expiry_time = ranks.expiry_time AND lr.owner_id = ranks.owner_id;

    nk.sqlExec(query, [leaderboardId, expiryTime, ownerId, rank]);

The example is in TypeScript but it should be easy to extrapolate it to Go.

You’d then need to check if a rank is present in the metadata, and if so use that value instead.

Hope this helps.

1 Like

Hi, @sesposito!

Ok, that makes sense. I was considering using a leaderboardReset hook to store the rank and expiry date on the player storage, but this seems like a much better approach.

Thanks a lot for your help!