DataStore Leaderboard Overloading

This leaderboard system is loading the same pages over and over until it overloads. I want this to load only the necessary pages until it finished listing all of them. This leaderboard system takes code from the Roblox resources leaderboard model.

I’m not sure why it is repeatedly loading, but I found out that it is happening during the :AdvanceToNextPageAsync() function.

Leaderboard Script:

local replicatedstorage = game:GetService("ReplicatedStorage")
local datastoreservice = game:GetService("DataStoreService")

local services = replicatedstorage:FindFirstChild("Services")

local leaderboardsystem = require(services:FindFirstChild("LeaderboardSystem"))

local winsLeaderboardData = datastoreservice:GetOrderedDataStore("Wins")
local shredsLeaderboardData = datastoreservice:GetOrderedDataStore("Shreds")
local donationsLeaderboardData = datastoreservice:GetOrderedDataStore("Donations")

local winsBoardModel = workspace:FindFirstChild("WinsLeaderboard")
local shredsBoardModel = workspace:FindFirstChild("ShredsLeaderboard")
local donationsBoardModel = workspace:FindFirstChild("DonationsLeaderboard")

local winsBoard = leaderboardsystem.new(winsBoardModel, winsLeaderboardData, true, "Wins")
local shredsBoard = leaderboardsystem.new(shredsBoardModel, shredsLeaderboardData, true, "Shreds")
local donationsBoard = leaderboardsystem.new(donationsBoardModel, donationsLeaderboardData, false)

winsBoard:Init()
shredsBoard:Init()
donationsBoard:Init()

Leaderboard Module:

local MarketplaceService = game:GetService("MarketplaceService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")

local modules = ReplicatedStorage:FindFirstChild("Modules")

local formatLargeNumber = require(modules.formatLargeNumber)
local retryAsync = require(modules.retryAsync)

local USER_ICON_TEMPLATE = "rbxthumb://type=AvatarHeadShot&id=%d&w=60&h=60"
local ROBUX_TEMPLATE = utf8.char(0xE002) .. " %s"

local DISPLAY_COUNT = 100
local UPDATE_INTERVAL = 120
local UPDATE_MAX_ATTEMPTS = 3 -- Make up to 3 attempts (Initial attempt + 2 retries)
local UPDATE_RETRY_PAUSE_CONSTANT = 1 -- Base wait time between attempts
local UPDATE_RETRY_PAUSE_EXPONENT_BASE = 2 -- Base number raised to the power of the retry number for exponential backoff

local leaderboard = {}
leaderboard.__index = leaderboard

function leaderboard.new(board, datastore, autoUpdate, dataName)
	local newBoard = setmetatable({}, leaderboard)
	newBoard.Board = board
	newBoard.DataStore = datastore
	newBoard.AutoUpdate = autoUpdate
	newBoard.DataName = dataName
	newBoard.DisplayFrames = {}
	newBoard.UserIdUsernameCache = {}
	
	return newBoard
end

function leaderboard:Init()
	self:CreateDisplayFrames()

	task.spawn(function()
		while true do
			if self.AutoUpdate and not RunService:IsStudio() then
				self:UpdateData()
			end
			
			self:RefreshLeaderboardAsync()
			task.wait(UPDATE_INTERVAL)
		end
	end)
end

function leaderboard:UpdateData()
	for _, player in pairs(Players:GetPlayers()) do
		if player:GetAttribute(self.DataName) == 0 then continue end

		retryAsync(function()
			self.DataStore:UpdateAsync(player.UserId, function()
				return player:GetAttribute(self.DataName)
			end)
		end, UPDATE_MAX_ATTEMPTS, UPDATE_RETRY_PAUSE_CONSTANT, UPDATE_RETRY_PAUSE_EXPONENT_BASE)
	end
end

function leaderboard:GetUsernameFromUserIdAsync(userId: number): string
	if self.UserIdUsernameCache[userId] then
		return self.UserIdUsernameCache[userId]
	else
		local success, result = pcall(function()
			return Players:GetNameFromUserIdAsync(userId)
		end)
		if success then
			self.UserIdUsernameCache[userId] = result
			return result
		else
			return string.format("<unknown%d>", userId)
		end
	end
end

-- Update a display frame with the specified userId and robuxAmount
function leaderboard:UpdateDisplayFrameInfoAsync(displayFrame: any, userId: number?, data: number?)
	if userId and data then
		local displayUsername = self:GetUsernameFromUserIdAsync(userId)
		local displayAmount = string.format(ROBUX_TEMPLATE, formatLargeNumber(data))
		local displayIcon = string.format(USER_ICON_TEMPLATE, userId)

		displayFrame.UserDisplay.NameLabel.Text = displayUsername
		displayFrame.UserDisplay.IconLabel.Image = displayIcon
		displayFrame.DataLabel.Text = displayAmount

		displayFrame.Name = displayUsername
		displayFrame.Visible = true
	else
		displayFrame.Visible = false
	end
end

-- Create necessary display frames to be used on the leaderboard
function leaderboard:CreateDisplayFrames()
	for i = 1, DISPLAY_COUNT do
		local isEven = i % 2 == 0
		local displayFrame = self.Board.Template:Clone()
		displayFrame.BackgroundTransparency = if isEven then 0.9 else 1
		displayFrame.LayoutOrder = i
		displayFrame.UserDisplay.RankLabel.Text = tostring(i)
		displayFrame.Visible = false
		displayFrame.Parent = self.Board.Board :: any

		self.DisplayFrames[i] = displayFrame
	end
end

-- Retreive the top <DISPLAY_COUNT> donors and display their information on the leaderboard
function leaderboard:RefreshLeaderboardAsync()
	local success, result = pcall(function()
		-- DataStorePages support up to 100 items per page, so page size must be clamped in the case of DISPLAY_COUNT > 100
		return self.DataStore:GetSortedAsync(false, math.min(DISPLAY_COUNT, 100))
	end)
	
	if not success then
		warn(string.format("Failed to retrieve leaderboard data because: %s", result))
		return
	end

	local pages = result :: DataStorePages
	local top = {}

	-- Pull items from the pages object until we have enough to satisfy DISPLAY_COUNT or run out of pages
	repeat
		local currentPage = pages:GetCurrentPage()
		for _, data in currentPage do
			table.insert(top, data)
		end
		if pages.IsFinished or #currentPage == 0 then
			break
		else
			pages:AdvanceToNextPageAsync()
		end
	until #top >= DISPLAY_COUNT

	for i = 1, DISPLAY_COUNT do
		local donorData = top[i]
		local displayFrame = self.DisplayFrames[i]

		if donorData then
			local userId = donorData.key
			local dataAmount = donorData.value
			self:UpdateDisplayFrameInfoAsync(displayFrame, userId, dataAmount)
		else
			self:UpdateDisplayFrameInfoAsync(displayFrame, nil, nil)
		end
	end
end

return leaderboard

1 Like

Nothing’s really jumping out at me here. My last hypothesis would be some sort of threading bug embedded in retryAsync that is resuming the thread yielding on task.wait. Try disabling auto updates on all of your leaderboards. Have you attempted using breakpoints to get a formal look at how your script is downloading leaderboard data?

1 Like

I’ve never used breaks, but I have used printing.

1 Like

Using task.wait, it just keeps loading in the last page in the list but never turns IsFinished to true.

1 Like

I did not ask you to use task.wait anywhere. Use breakpoints to investigate the downloading code. Attempt to disable auto updates

1 Like

I used the break points to find top 15 because of how much it kept loading the same user over and over, all it did was load in the same user on the leaderboard. I’m not sure where else to put break points to find the problem. I did find out that it normally loads on the Wins leaderboard and everything else is repeated.

1 Like

But a breakpoint on the break keyword in your loop to confirm it is ending. You can also print out the index of the item to see how it’s progressing pages

1 Like

The indexes for shreds and donations leaderboard do not stop at all and does not progress pages at all and stays at the first one. The only one that behaves correctly is the wins leaderboard.

1 Like

Can you send GIFs or images? It would be easier to coordinate with your code rather than your statements

1 Like

The leaderboards based on all the indexes


The page stays on the first index
Screenshot 2025-03-08 234839

1 Like

This was the printed updated code:


	repeat
		local currentPage = pages:GetCurrentPage()
		for _, data in currentPage do
			table.insert(top, data)
		end
		if pages.IsFinished or #currentPage == 0 then
			break
		else
			pages:AdvanceToNextPageAsync()
		end
		print(self.DataStore, "Page", #currentPage)
	until #top >= DISPLAY_COUNT
1 Like

#currentPage does not tell you about the objective index of the item. It tells you how many items are in the current page. You need to make your own incremental value, or print out the index in the the loop through currentPage’s contents

1 Like

oh

if I were to make my own, that would go until it errors or until it reaches max because it won’t stop until the loop is broken. I’m assuming that the page isn’t advancing because it’s the same player each time and the page isFinished bool is false. It usually forcefully stops at 16 or 17

No wait:
Screenshot 2025-03-09 000057
1 second wait:
Screenshot 2025-03-09 000157

1 Like

This is the code I used to print out the indexes:

	local pg = 0
	repeat
		local currentPage = pages:GetCurrentPage()
		for _, data in currentPage do
			table.insert(top, data)
		end
		if pages.IsFinished or #currentPage == 0 then
			break
		else
			task.wait(1)
			pages:AdvanceToNextPageAsync()
			pg += 1
		end
		
		print(pg)
	until #top >= DISPLAY_COUNT

The users do not display on the leaderboard if it gives a warning or error

1 Like

Hm. Things just aren’t adding up. Can you add me to the place so I can take a full crack at this? If not, add me on Discord @ziffix and I’ll walk you through my personal debugging steps

1 Like

I’ll need to look at your data. A copy won’t carry that over. I can try emulating if nothing else works for you

1 Like

Join the group, it won’t let me give you access until then.

Here’s the game:
The Great Cheese War - Roblox

1 Like

I have joined the group. Let me know when I have access

1 Like

LeaderboardHandler is in ServerScriptService
LeaderboardSystem is in ReplicatedStorage → Services

1 Like

Reply to this post with your changes once youre finished

1 Like