So I have a game that’s been snowballing up in visits over the years. It’s not a game I’m necessarily proud of, but I continue adding updates to the small and dedicated fanbase it has.
My absolute worst issue is with datastores. I love them. I hate them. Over the past few years, I have gotten a significant number of warnings. I am aware that the warnings don’t mean anything since the saving works well. But my LORD is it a pain to see in the output.
I have tried wrapping it in pcalls and whatnot, but the warning message persists. I don’t believe that I’m calling the datastore every milisecond, either. There’s a leaderboard that refreshes every 30 seconds (and looks up to rank #1000 for what position you’re in) and the saving for the game itself only saves when you leave the game. The datastores in question are leaderboard datastores (tccdatastore, HighscoresCC_1)
edit - This leaderboard system I have works fine for one leaderboard, but I wonder if the request queue has something to do with the other leaderboard, as the search query to rank 1000 doesn’t work there.
any suggestions to a more efficient method to the above or removing the warnings would be much appreciated.
To the young children looking at this post in twenty years, there is no way of removing them. Just optimize the code in other ways.
You can’t really get rid of them because they are there for a reason. You’re definetly using the DataStores in an inefficient way. It would be nice if you could share your code for the leaderboards.
First I’m going to say that you are using return in your loops when it seems to me that you should be using continue.
You are calling GetSortedAsync so many unnecessary times that it scares me. Why are you calling GetSortedAsync for every player? You could should call it once outside the loop.
Additionally you’re calling GetAsync on the OrderedDataStore many times, consider caching that as well.
Also note GetNameFromUserIdAsync should be pcalled, you can’t just pcall the entire function and expect it to work; even if one asynchronous method fails, the entire function will stop working. Fix: use pcall on every single asynchronous method instead of just one for the entire function.
Okay well no, you cannot silence the requests. The reason why is probably because they are so vital to your game and not being able to get/set data could possibly break it, that Roblox must let you know if there are issues.
return exits the whole function, so if you put it inside a loop, it’ll stop the entire leaderboard update. continue just skips to the next item in the loop. So for skipping invalid entries, you should use continue.
GetSortedAsync() always starts at the first page, which is why you should only call it once, and then use AdvanceToNextPageAsync() to loop through the pages as needed.
I’m sure you just could call it once and then have a table that is filled with all the data and then loop through that table for each player. Example:
local leaderboardData = {}
local pages = store:GetSortedAsync(false, 100)
repeat
local currentPage = pages:GetCurrentPage()
for rank, data in ipairs(currentPage) do
leaderboardData[tonumber(data.key)] = {
rank = rank,
value = data.value
}
end
if not pages.IsFinished then
pages:AdvanceToNextPageAsync()
end
until pages.IsFinished
for _, player in pairs(game.Players:GetPlayers()) do
--go through the leaderboardData table and match it with the player's userId
end
-- Single‑Worker Save Queue Example (serial task queue)
local DataStoreService = game:GetService("DataStoreService")
local MyDataStore = DataStoreService:GetDataStore("PlayerData")
-- Our queue of pending saves
local SaveQueue = {}
local IsProcessing = false
-- Enqueue a save request
local function EnqueueSave(userId: number, data: table)
-- push to the end of the queue
SaveQueue[#SaveQueue + 1] = {
Key = "Player_" .. userId,
Data = data,
}
-- start the processor if it's not already running
if not IsProcessing then
IsProcessing = true
task.spawn(ProcessQueue)
end
end
-- The single worker that processes the queue
function ProcessQueue()
while #SaveQueue > 0 do
-- check how many writes we have budget for
local budget = DataStoreService:GetRequestBudgetForRequestType(
Enum.DataStoreRequestType.SetAsync
)
if budget > 0 then
-- pull the oldest request
local request = table.remove(SaveQueue, 1)
local success, err = pcall(function()
MyDataStore:SetAsync(request.Key, request.Data)
end)
if not success then
warn("Save failed for", request.Key, "– requeuing:", err)
-- put it back at the front for retry
table.insert(SaveQueue, 1, request)
-- back off a bit before retry
task.wait(1)
end
else
-- no budget left, wait for budget to replenish
task.wait(0.1)
end
-- yield so we don’t block other tasks
task.wait()
end
IsProcessing = false
end
-- Example usage:
-- whenever you want to save:
-- EnqueueSave(player.UserId, {Level = 5, Coins = 120})
I do suggest though utilizing some public modules like ProfileStore, they take away the pain of datastores with a much friendlier and easier system of just mutable tables. It handles the rest.
Well yeah that would work but it is ignoring the underlying issue of the inefficient code. It would also take the script much longer to process the data
Try something like this to replace the leaderboardupdate function (well he can but yeah)
local DataStoreService = game:GetService("DataStoreService")
-- Wait until we have at least one read‐budget before proceeding
local function waitForReadBudget()
while DataStoreService:GetRequestBudgetForRequestType(
Enum.DataStoreRequestType.GetAsync
) < 1 do
task.wait(0.1)
end
end
function leaderboardupdate(which, total, storeName)
local store = DataStoreService:GetOrderedDataStore(storeName)
-- Helper to fetch a sorted page
local function fetchPage()
waitForReadBudget()
return store:GetSortedAsync(false, 100)
end
-- Helper to fetch a single value
local function getValue(userId)
waitForReadBudget()
return store:GetAsync(userId)
end
pcall(function()
-- 1) Pull the top entries
local pages = fetchPage()
local topEntries = pages:GetCurrentPage()
-- Clear out old UI
local rankedGui = which.Gui.Ranked
local children = rankedGui:GetChildren()
for i = 1, #children do
local v = children[i]
if not v:IsA("UIListLayout") then
v:Destroy()
end
end
task.wait(2)
-- Populate the top‐N
for rank = 1, #topEntries do
if rank > total then break end
local data = topEntries[rank]
local id = tonumber(data.key)
if not id or id <= 0 then break end
local username = game.Players:GetNameFromUserIdAsync(id)
if not username then break end
-- Award badge if they don’t have it
if game.Players:FindFirstChild(username)
and not game:GetService("BadgeService")
:UserHasBadgeAsync(id, 1368874078045844)
then
game.ReplicatedStorage.Badges:Fire(id, 1368874078045844)
end
local chart = which.Gui.Template:Clone()
if which == workspace.LeaderboardCC then
chart.Cubes.Text = data.value / 100
else
chart.Cubes.Text = convNumberLEADERBOARD(data.value)
end
chart.Rank.Text = tostring(rank).."."
chart.Username.Text = username
chart.Visible = true
chart.Parent = rankedGui
end
-- 2) Send each player their own position/value
local players = game.Players:GetPlayers()
for _, plr in players do
local found = false
local iterPages = fetchPage()
-- Search up to 10 pages for their entry
for _ = 1, 10 do
local pageData = iterPages:GetCurrentPage()
for j = 1, #pageData do
local entry = pageData[j]
if tonumber(entry.key) == plr.UserId then
local value = getValue(plr.UserId)
local displayRank = tostring(j).."."
local displayValue = (which == workspace.LeaderboardCC)
and (value/100)
or convNumberLEADERBOARD(value)
local event = (which == workspace.LeaderboardCC)
and game.ReplicatedStorage.LeaderboardPositions2
or game.ReplicatedStorage.LeaderboardPositions
event:FireClient(plr, which, displayRank, displayValue)
found = true
break
end
end
if found then break end
waitForReadBudget()
iterPages = iterPages:AdvanceToNextPageAsync()
end
-- If still not found, default to “1001+”
if not found then
local value = getValue(plr.UserId)
local displayValue = (which == workspace.LeaderboardCC)
and (value/100)
or convNumberLEADERBOARD(value)
game.ReplicatedStorage.LeaderboardPositions
:FireClient(plr, which, "1001+", displayValue)
end
end
end)
end
The issue is that once you run out of pages, your call to AdvanceToNextPageAsync() can return nil, and on the next loop you do iterPages:GetCurrentPage() on that nil value. I kinda missed it myself. Just add a guard to that.
Its messier in practice I was just prettying it up for presentation purposes lol
I admit though I wanna do other things than be on the devforums (ive been helping people for about an hour now) so i can maybe reply tomorrow or later tonight if the issue is still plaguing you
Haha all good. I figured it out myself. Can’t express words of gratitude for helping me out today. Enjoy your evening. @7eoeb bless you as well for your support.