How do I make a global leaderboard in Roblox?
Use an OrderedDataStore, write the score when it changes, and read the top with GetSortedAsync on a timer — once a minute for the whole server, not once per player. Cache the page, broadcast it to clients, and keep names out of the store: look them up by user id.
In short
- OrderedDataStore stores numbers only. Keep the names elsewhere.
- Read on a timer for the server, never on join for each player.
- Cache the page in memory; every client reads the same cached list.
A leaderboard is a read problem, not a write problem. Writing one number per player is cheap. Reading the top ten is the part that gets a game throttled, because the obvious implementation reads it once per player per join — which on a full server is fifty reads for one list that is the same for everybody.
Steps
1. Write the score where it can be sorted
An OrderedDataStore holds integers and nothing else. That is the constraint that shapes the whole design.
local DataStoreService = game:GetService("DataStoreService")
local board = DataStoreService:GetOrderedDataStore("Coins_AllTime_v1")
local function publishScore(userId: number, score: number)
-- Integers only: a float is refused, and so is a negative in a
-- leaderboard you intend to read descending.
local value = math.max(0, math.floor(score))
local ok, err = pcall(function()
board:SetAsync(tostring(userId), value)
end)
if not ok then
warn(`[board] write failed for {userId}: {err}`)
end
end
2. Read the top on a timer, once for the server
One read, cached, shared. The page size is the thing you show; asking for a hundred to display ten is a hundred rows you pay for.
local Players = game:GetService("Players")
local TOP_N = 10
local REFRESH = 60
local cached: { { userId: number, score: number, name: string } } = {}
local function refresh()
local ok, pages = pcall(function()
return board:GetSortedAsync(false, TOP_N) -- false = descending
end)
if not ok then return end
local rows = {}
for rank, entry in pages:GetCurrentPage() do
local userId = tonumber(entry.key)
if userId then
-- The name is NOT in the store: it changes, and storing it means a
-- rename shows the old one forever.
local nameOk, name = pcall(function()
return Players:GetNameFromUserIdAsync(userId)
end)
rows[rank] = {
userId = userId,
score = entry.value,
name = nameOk and name or `User {userId}`,
}
end
end
cached = rows
end
3. Broadcast the cached page
The clients never read the store. They are handed the same list the server already has.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local feed = Instance.new("RemoteEvent")
feed.Name = "Leaderboard"
feed.Parent = ReplicatedStorage
task.spawn(function()
while true do
refresh()
feed:FireAllClients(cached)
task.wait(REFRESH)
end
end)
-- A player who joins between refreshes gets the cache, not a new read.
Players.PlayerAdded:Connect(function(player: Player)
task.defer(function()
feed:FireClient(player, cached)
end)
end)
4. Draw it
Ten rows, the number first. A leaderboard where the score is smaller than the name is a leaderboard nobody reads.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local feed = ReplicatedStorage:WaitForChild("Leaderboard")
local list = script.Parent:WaitForChild("Rows") -- a Frame with a UIListLayout
local template = script.Parent:WaitForChild("RowTemplate")
feed.OnClientEvent:Connect(function(rows)
list:ClearAllChildren()
for rank, row in rows do
local line = template:Clone()
line.Name = string.format("%02d", rank)
line.Rank.Text = `#{rank}`
line.PlayerName.Text = row.name
line.Score.Text = string.format("%d", row.score)
line.Visible = true
line.Parent = list
end
end)
Which store for which board
| What you want | Store | Why |
|---|---|---|
| All-time top 10 | GetOrderedDataStore, one key | Sorted reads are the only thing it does |
| This week's top 10 | One ordered store per week (..._w2026W41) | A window is a new key, never a filter |
| A player's own rank | A read of their key plus the top page | Exact rank for everybody is not a DataStore feature |
| Top by a non-number (a title, a pet) | A normal DataStore plus your own sort | Ordered stores hold integers only |
FAQ
Why is my leaderboard empty in Studio?
Studio can reach DataStores only when "Enable Studio Access to API Services" is on in Game Settings, and an OrderedDataStore in Studio starts empty because it is the live store and nothing in it was written by this place yet. Publish once, play in the real game, and it fills.
How do I do a weekly leaderboard?
Put the window in the key: Coins_2026W41. On Monday the name changes, the old store keeps last week's and the new one starts empty. Deriving the key from os.date("!*t") means nobody has to remember to rotate it.
Can I store the player's name in the store?
You can, but do not. Names change, and the store would keep showing the old one. GetNameFromUserIdAsync is a cheap lookup and it is correct — wrap it in pcall, because it is somebody else's service.
How often can I write?
Think in a budget per server, not per player: roughly 60 writes a minute. Write when the score actually changes and on leave, never every time a coin is picked up.
Updated: Tested in Studio: Roblox Studio, October 2026Author: GustavoLicense: Code in this guide may be used in any Roblox experience, including a paid one
Related pack
