Monthly Leaderboard script not working

I am trying to make a monthly leaderboard. However, it almost never shows a player, and when it does, it displays the all-time survival count, not how many were gained in a given month

The code for the script is below:

local ds = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local DATASTORE_SCOPE = "global"
local SurvivalStats = ds:GetOrderedDataStore("survivalStats", DATASTORE_SCOPE)

local playerStartingSurvivals = {}

local CST_OFFSET = -6 * 3600

local function getCSTTime()
    return os.time() + CST_OFFSET
end

local function getCurrentMonthKey()
    local dateInfo = os.date("*t", getCSTTime())
    return string.format("%04d-%02d", dateInfo.year, dateInfo.month)
end

local function getTimeUntilMonthEnd()
    local dateInfo = os.date("*t", getCSTTime())
    local nextMonth = dateInfo.month + 1
    local nextYear = dateInfo.year
    if nextMonth > 12 then
        nextMonth = 1
        nextYear = nextYear + 1
    end
    local nextMonthTime = {
        year = nextYear,
        month = nextMonth,
        day = 1,
        hour = 0,
        min = 0,
        sec = 0
    }
    local nextMonthTimestamp = os.time(nextMonthTime) - CST_OFFSET
    local currentTimestamp = getCSTTime()
    return nextMonthTimestamp - currentTimestamp
end

local currentMonthKey = getCurrentMonthKey()
local timeUntilReset = getTimeUntilMonthEnd()

local function loadPlayerMonthlySurvival(player)
    local dataKey = tostring(player.UserId) .. "_" .. currentMonthKey
    local monthlySurvivals = 0
    
    local success, result = pcall(function()
        return SurvivalStats:GetAsync(dataKey)
    end)
    
    if success and result then
        monthlySurvivals = result
    end
    
    return monthlySurvivals
end

local function savePlayerSurvival(player)
    local leaderstats = player:FindFirstChild("leaderstats")
    if leaderstats then
        local survivals = leaderstats:FindFirstChild("Survivals")
        if survivals then
            local startingSurvivals = playerStartingSurvivals[player.UserId] or 0
            local monthlySurvivals = math.max(0, survivals.Value - startingSurvivals)
            
            local dataKey = tostring(player.UserId) .. "_" .. currentMonthKey
            local success, err = pcall(function()
                SurvivalStats:SetAsync(dataKey, monthlySurvivals)
            end)
            if not success then
                warn("Failed to save survival stats for player " .. player.Name .. ": " .. err)
            end
        end
    end
end

local function updateLeaderboardDisplay()
    local children = script.Parent:GetChildren()
    for i = 1, #children do
        local leaderboardRank = children[i]
        if leaderboardRank:IsA("Frame") then
            leaderboardRank:Destroy()
        end
    end

    local success, errorMsg = pcall(function()
        local data = SurvivalStats:GetSortedAsync(false, 10)
        local SurvivalsPage = data:GetCurrentPage()

        for rankInLB = 1, #SurvivalsPage do
            local dataStored = SurvivalsPage[rankInLB]
            local dataKey = tostring(dataStored.key)
            local underscorePos = dataKey:find("_")
            if underscorePos then
                local userIdStr = dataKey:sub(1, underscorePos - 1)
                local monthKey = dataKey:sub(underscorePos + 1)
                
                if monthKey == currentMonthKey then
                    local userId = tonumber(userIdStr)
                    if userId then
                        local name = Players:GetNameFromUserIdAsync(userId)
                        if name then
                            local Survivals = dataStored.value

                            local template = script:FindFirstChild("Template")
                            if template then
                                local newTemplate = template:Clone()
                                newTemplate.Name = name .. "Leaderboard"
                                newTemplate.PlrName.Text = name
                                newTemplate.Rank.Text = "#" .. rankInLB
                                newTemplate.Survivals.Text = Survivals
                                newTemplate.Parent = script.Parent
                            end
                        end
                    end
                end
            end
        end
    end)

    if not success then
        warn("Error updating leaderboard: " .. errorMsg)
    end
end

local function trackPlayerSurvivals(player)
    local leaderstats = player:WaitForChild("leaderstats", 5)
    if not leaderstats then return end
    
    local survivals = leaderstats:WaitForChild("Survivals", 5)
    if not survivals then return end
    
    if playerStartingSurvivals[player.UserId] == nil then
        local existingMonthlySurvivals = loadPlayerMonthlySurvival(player)
        playerStartingSurvivals[player.UserId] = survivals.Value - existingMonthlySurvivals
    end
    
    savePlayerSurvival(player)
    
    survivals.Changed:Connect(function(newValue)
        savePlayerSurvival(player)
        updateLeaderboardDisplay()
    end)
end

Players.PlayerAdded:Connect(function(player)
    player.CharacterAdded:Connect(function()
        task.wait(1)
        trackPlayerSurvivals(player)
    end)
    
    task.spawn(function()
        task.wait(2)
        trackPlayerSurvivals(player)
    end)
end)

for _, player in Players:GetPlayers() do
    task.spawn(function()
        trackPlayerSurvivals(player)
    end)
end

while task.wait(1) do
    timeUntilReset = timeUntilReset - 1
    
    local days = math.floor(timeUntilReset / 86400)
    local hours = math.floor((timeUntilReset % 86400) / 3600)
    local minutes = math.floor((timeUntilReset % 3600) / 60)
    local seconds = timeUntilReset % 60
    
    local timeString = string.format("%dd %dh %dm %ds", days, hours, minutes, seconds)
    script.Parent.Parent.Parent.ResetTime.Text = "Month ends in: " .. timeString

    local newMonthKey = getCurrentMonthKey()
    if newMonthKey ~= currentMonthKey or timeUntilReset <= 0 then
        currentMonthKey = newMonthKey
        timeUntilReset = getTimeUntilMonthEnd()
        
        local players = Players:GetPlayers()
        for i = 1, #players do
            local plr = players[i]
            local leaderstats = plr:FindFirstChild("leaderstats")
            if leaderstats then
                local survivals = leaderstats:FindFirstChild("Survivals")
                if survivals then
                    playerStartingSurvivals[plr.UserId] = survivals.Value
                end
            end
        end
        
        for i = 1, #players do
            local plr = players[i]
            savePlayerSurvival(plr)
        end
        
        updateLeaderboardDisplay()
    end
end

Any and all help is appreciated!

GetsortedAsync returns ALL months. Your datastore keys look like this:

UserId_YYYY-MM
Example:
12345678_2026-03

But when you run:
local data = SurvivalStats:GetSortedAsync(false, 10)

It returns the top 10 values across ALL keys, including previous months.

Then your code filters by checking if monthKey is equal to currentMonthKey but by the time you filter them, he top results may already older months.

Use the month as a datastore scope instead:
local SurvivalStats = ds:GetOrderedDataStore("survivalStats_" .. currentMonthKey)

Then your keys can just be UserId
Example:
local dataKey = tostring(player.UserId)

Now the ordered datastore only contains this months date.


Your monthly calculation can break as well.
playerStartingSurvivals[player.UserId] = survivals.Value - existingMonthlySurvivals

This assumes the player’s all-time survivals never changed outside the script. But if leaderboards load before leaderstats, survivals update during loading, or a server restart happens, the value can become incorrect and show all-time stats.

Instead just store the monthly number directly

Another thing I noticed is that you called trackPlayerSurvivals twice. The function can run twice per player which can break starting values. Just do on PlayerAdded and call the function once.


Wrap this: local name = Players:GetNameFromUserIdAsync(userId) in a pcall()
or else it may throttle if called many times.

Other than that, you can definetly write a cleaner monthly leaderboard than what you have currently. I would recommend you learn time and date handling and script organization.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.