Scores & dataBeginner plusNo Robux needed

Build a Global Leaderboard in Roblox Studio

Save Score values in an Ordered Data Store and display the ten highest scores from every server on an in-game leaderboard.

Published
A global leaderboard in Roblox Studio displaying three player scores in descending order

Display the ten highest Scores from every server on one board inside the game. The finished board orders values from highest to lowest, such as 1300, 900, and 500, and refreshes every 60 seconds. This takes about eight minutes and does not require Robux.

A global leaderboard showing three Scores from highest to lowest

Figure guide: The Japanese board title means Global Leaderboard. The smaller line says it includes all servers and refreshes every 60 seconds; the three rows show ranks 1–3 with Scores 1300, 900, and 500.

Start after completing the Score persistence guide. Use a dedicated published test experience where leaderstats > Score and SaveScore work and Studio Access to API Services is enabled.

As Roblox explains in its Studio access guidance, Studio connects to cloud data for that experience. Do not enable this tutorial against a live production data set; use the dedicated test experience from the prerequisite.

1. Build the leaderboard board

Stop Play. Add a Part to Workspace, rename it GlobalLeaderboardBoard, turn on Anchored, turn off CanCollide, and set Size to 18, 12, 1.

Add a SurfaceGui inside the Part and a Frame named Panel inside the SurfaceGui. Add three TextLabel objects under Panel and rename them Title, Status, and Entries. You may also add a thin Frame named Divider.

Workspace
└─ GlobalLeaderboardBoard (Part)
   └─ SurfaceGui
      └─ Panel (Frame)
         ├─ Title (TextLabel)
         ├─ Status (TextLabel)
         ├─ Divider (Frame)
         └─ Entries (TextLabel)

The SurfaceGui and TextLabel hierarchy for GlobalLeaderboardBoard

Set SurfaceGui.CanvasSize to 900, 600 and turn on AlwaysOnTop. Make each TextLabel background transparent, align its text left, and enter these values:

Name Text Position Size
Title Global Leaderboard 40, 26 820, 64
Status Waiting for refresh... 42, 92 816, 34
Entries No records yet 48, 150 804, 408

Enter every Position and Size value as X and Y Offsets. For example, the Title Position uses X 40 and Y 26. A yellow GothamBold Title, cyan Status, and white Entries label match the visual separation in the screenshot. Set Entries.TextYAlignment to Top so additional rows grow downward.

If ten rows do not fit, reduce Entries.TextSize to about 25. If the board’s front looks blank, change SurfaceGui.Face from Front to Back or rotate the Part 180 degrees. Make sure Waiting for refresh... is visible before adding code; otherwise a successful read and an invisible UI look like the same failure.

Tip — SurfaceGui places UI on a Part

A SurfaceGui displays text and controls on a Part’s surface. Unlike a ScreenGui, it stays in the 3D world, so this one works as a physical notice board.

2. Save each departing player’s Score

Add a Script to ServerScriptService, rename it GlobalScoreWriter, and paste:

local Players = game:GetService("Players")
local DataStoreService = game:GetService("DataStoreService")

local rankingStore = DataStoreService:GetOrderedDataStore("GlobalScore_v1")
local lastSavedValue = {}

local function saveScore(player)
	local leaderstats = player:FindFirstChild("leaderstats")
	local score = leaderstats and leaderstats:FindFirstChild("Score")
	if not score then
		return
	end

	local userId = player.UserId
	local value = score.Value
	if lastSavedValue[userId] == value then
		return
	end

	local success, saveError = pcall(function()
		rankingStore:SetAsync(tostring(userId), value)
	end)

	if success then
		lastSavedValue[userId] = value
		print("Global score saved:", player.Name, value)
	else
		warn("Global score save failed:", player.Name, saveError)
	end
end

Players.PlayerRemoving:Connect(saveScore)

game:BindToClose(function()
	for _, player in Players:GetPlayers() do
		saveScore(player)
	end
end)

The standard Data Store from the prerequisite can store numbers, strings, and tables. GetOrderedDataStore("GlobalScore_v1") opens a separate store whose numeric values can be sorted. Following Roblox’s leaderboard storage example, the key is the player’s UserId converted to text and the value is the numeric Score.

Tip — An Ordered Data Store sorts numeric values

An Ordered Data Store can return stored numbers in value order. If three players have 500, 1300, and 900, the leaderboard can request them as 1300, 900, 500.

lastSavedValue prevents the same server shutdown from writing an unchanged Score twice. SetAsync() remains inside pcall() because a cloud request can fail.

The Script reads the server’s leaderstats > Score; it never accepts a rank or balance supplied by a player’s screen. A LocalScript may change its own display, but that change cannot enter the shared ranking.

3. Display the top ten in descending order

Add another Script to ServerScriptService, rename it GlobalLeaderboard, and paste:

local Players = game:GetService("Players")
local DataStoreService = game:GetService("DataStoreService")

local rankingStore = DataStoreService:GetOrderedDataStore("GlobalScore_v1")
local panel = workspace
	:WaitForChild("GlobalLeaderboardBoard")
	:WaitForChild("SurfaceGui")
	:WaitForChild("Panel")
local entriesLabel = panel:WaitForChild("Entries")
local statusLabel = panel:WaitForChild("Status")

local REFRESH_SECONDS = 60
local previousText = entriesLabel.Text

local function getDisplayName(userId)
	local success, name = pcall(function()
		return Players:GetNameFromUserIdAsync(userId)
	end)
	if success then
		return name
	end
	return "User " .. userId
end

local function getTopTen()
	local success, pagesOrError = pcall(function()
		return rankingStore:GetSortedAsync(false, 10)
	end)
	if not success then
		return nil, pagesOrError
	end

	local lines = {}
	for rank, entry in ipairs(pagesOrError:GetCurrentPage()) do
		local userId = tonumber(entry.key)
		local name = userId and getDisplayName(userId) or entry.key
		table.insert(lines, string.format(
			"%d.  %-18s %d",
			rank,
			name,
			entry.value
		))
	end

	if #lines == 0 then
		return "No records yet"
	end
	return table.concat(lines, "\n")
end

local function refresh()
	statusLabel.Text = "Updating..."

	local newText, readError = getTopTen()
	if newText then
		previousText = newText
		entriesLabel.Text = newText
		statusLabel.Text = "All servers • refreshes every 60 seconds"
	else
		entriesLabel.Text = previousText
		statusLabel.Text = "Update failed. Showing previous ranking"
		warn("Global leaderboard read failed:", readError)
	end
end

while true do
	refresh()
	task.wait(REFRESH_SECONDS)
end

In GetSortedAsync(false, 10), false requests the largest values first and 10 limits the first page to ten records. The OrderedDataStore reference documents those arguments.

Tip — Descending order means largest to smallest

A descending Score list is 1300, 900, 500. Changing false to true reverses the order, so keep false for a high-score board.

GetNameFromUserIdAsync() turns each stored UserId into a player name. If only that lookup fails, the row falls back to a label such as User 12345 instead of discarding the whole ranking.

Both Scripts must use the exact store name GlobalScore_v1. They are opening the same storage location—one writes and the other reads. A typo such as GlobalScores_v1 makes the reader Script see a different, empty store.

If fewer than ten players have saved Scores, display only the available rows. Once eleven or more records exist, the first page contains the ten highest.

4. Save a Score and check the ranking

Press Play, change Score by collecting coins, and then press Stop. Output should show Global score saved: followed by the player name and Score.

Start Play again and look at the board. A cloud update can take a few seconds, so wait for the next 60-second refresh if the first result is old. To prove that the board is global, have other accounts join the same published test experience, change their Scores, and leave once. The rows should appear in descending Score order even when the writes came from different servers.

If other people need access to the test experience, first follow the friends-only sharing guide. The exact values do not need to be 1300, 900, and 500; their order only needs to match the saved Scores.

Roblox’s custom leaderboard tutorial notes that backend updates may take a few seconds. Do not duplicate the Script or repeatedly force reads while waiting.

When a ranking read fails, the Script keeps previousText on screen and changes only the Status label.

A failed refresh that preserves the previous three ranking rows

Figure guide: The Japanese Status line says Update failed. Showing previous ranking. The same three rows—1300, 900, and 500—remain visible beneath it.

The Data Store limits include an Ordered List request budget. Keep the 60-second interval instead of polling every few seconds. If the first read fails before any successful result exists, the board keeps No records yet; it can recover automatically on a later refresh.

Troubleshooting

  • It always says No records yet: Change Score, stop Play so the writer runs, start again, and wait through one refresh.
  • Output reports API access errors: Use the dedicated published test experience and check the Studio API setting from the prerequisite.
  • A row says User 12345: Only the name lookup failed. Keep the Score row and wait for another refresh.
  • The smallest Score is rank 1: Change GetSortedAsync(true, 10) back to false, 10.
  • The board is blank: Point SurfaceGui.Face toward the visible side and confirm that Title, Status, and Entries are inside Panel.
  • Rows disappear after a failed read: Keep entriesLabel.Text = previousText in the failure branch.

Completion check

  • Leaving the test experience saves the server’s Score to GlobalScore_v1.
  • The board displays up to ten records from highest to lowest.
  • UserIds are converted to names with a readable fallback.
  • Scores written from different servers share the same ranking.
  • A failed read keeps the last successful rows visible.
  • The refresh interval remains 60 seconds.

The game now has a persistent, all-server top-ten Score board.

Official sources

Expanded image