Skip to content
ENSign in

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 wantStoreWhy
All-time top 10GetOrderedDataStore, one keySorted reads are the only thing it does
This week's top 10One ordered store per week (..._w2026W41)A window is a new key, never a filter
A player's own rankA read of their key plus the top pageExact rank for everybody is not a DataStore feature
Top by a non-number (a title, a pet)A normal DataStore plus your own sortOrdered 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

Ultimate Script Pack

Ultimate Script Pack

$40See the pack

Get notified

Coming soon

Drop your email and we'll ping you the moment this pack goes live.