May 11, 2026
A Highrise Lua leaderboard system is a server-side code pattern in Highrise Studio that records player scores, validates scoring actions, and retrieves ranked entries using the Leaderboard API. The server should always be the score authority, not the client. This article walks through the definition, a working Lua scripting example for a Highrise leaderboard system, the difference between Leaderboard and Storage APIs, and the common mistakes that trip up new creators.
Break the phrase into three parts:
Lua scripting means writing Lua code to control game logic and interactivity inside Highrise Studio. Highrise uses Lua (not C#) as the supported scripting language for building interactive experiences, covering everything from object behavior to player input handling and game states.
Highrise leaderboard system refers to score and ranking logic built for a Highrise World. The Leaderboard API is designed specifically for player rankings, high-score tracking, and competitive game statistics across multiple boards (daily, weekly, all-time).
Example is the practical part. Creators searching this phrase want a small, copyable code pattern showing the flow: a player earns points, the server validates the action, the leaderboard updates, and rankings become available.
Put together, a Lua scripting example for a Highrise leaderboard system is a compact server-side Lua pattern that increments or sets player scores, stores them under a leaderboard ID, and retrieves ranked entries so players can compare performance.
| Term | Meaning |
|---|---|
| Server script | Code running on the server. Should handle all trusted score changes. |
| Client script | Code running on the player’s device. Should not be trusted for final score decisions. |
| Leaderboard ID | A string naming a leaderboard, like daily_scores or global_scores. |
| LeaderboardEntry | A record containing id, name, score, and rank. |
| Callback | A function passed into an API call that runs when the result is available. |
| Rate limit | A cap on how often API calls can be made before throttling kicks in. |
| NetworkValue | A synchronized value type for live UI updates across server and clients. |
The mental model is a pipeline with clear boundaries between what the client does and what the server controls.
Player performs a scoring action (collects coin, finishes race)
↓
Client detects or signals the action to the server
↓
Server validates the action (Does this coin exist? Already claimed?)
↓
Server calls IncrementScoreForPlayer or SetScoreForPlayer
↓
Server fetches top entries with GetEntries
↓
UI displays player names, scores, and ranks
This flow relies on client-server communication through events like FireServer and FireClient. The client requests, the server decides, and the leaderboard API records the result.
The most important rule is straightforward: the server is the score authority. The client can display scores or request actions, but the server validates everything before calling leaderboard methods. Highrise’s security best practices warn explicitly that client events can be intercepted and manipulated, making server-side validation essential.
If you’re new to building interactive Highrise Worlds, this architecture might feel like extra work. It’s not optional. Skipping server validation is the fastest path to a broken leaderboard.
Here is a Lua scripting example for a Highrise leaderboard system that awards points after a server-approved action. This is educational, not a production drop-in script, but it shows the correct pattern.
--!Type(Server)
local LEADERBOARD_ID = "global_scores"
local function awardPoints(player: Player, points: number)
if points <= 0 then
return
end
Leaderboard.IncrementScoreForPlayer(
LEADERBOARD_ID,
player,
points,
function(entry, error)
if error ~= LeaderboardError.None then
print("Leaderboard error: " .. tostring(error))
return
end
print(entry.name .. " score: " .. entry.score)
print("Rank: #" .. entry.rank)
end
)
end
server.PlayerConnected:Connect(function(player: Player)
print(player.name .. " joined the world.")
end)
-- Call this only after the server validates the scoring action.
function AwardCoinPoints(player: Player)
awardPoints(player, 10)
end
--!Type(Server) marks the script as server-side. Highrise script type attributes include Client, Server, Module, ClientAndServer, and UI.
LEADERBOARD_ID is the string that names this leaderboard. You can create multiple boards (like daily_scores and weekly_scores) just by using different strings.
IncrementScoreForPlayer adds points to a player’s existing score and returns the updated entry with the new rank and score in a callback. This is a server-only method.
The callback checks LeaderboardError.None before proceeding. The error enum also includes values for invalid IDs, throttling (RequestThrottled), and timeouts.
Notice that the client never sends a score amount. The server decides the point value (10 in this case). This follows Highrise’s security guidance directly.
A leaderboard is only useful if players can see it. Here’s how to fetch ranked entries:
--!Type(Server)
local LEADERBOARD_ID = "global_scores"
local function loadTopTen()
Leaderboard.GetEntries(LEADERBOARD_ID, 1, 10, function(entries, error)
if error ~= LeaderboardError.None then
print("Could not fetch leaderboard: " .. tostring(error))
return
end
for _, entry in ipairs(entries) do
print(entry.rank .. ". " .. entry.name .. ": " .. entry.score)
end
end)
end
GetEntries takes a leaderboard ID, a starting rank, and a limit. Starting at rank 1 with a limit of 10 gives you the standard top-10 list. Each entry includes rank, name, score, and id.
One thing to watch: do not call this every frame. Leaderboard API calls are rate limited, and excessive requests return RequestThrottled. Call it on meaningful events, like when a player opens the leaderboard UI or when a match ends.
This is where many creators get confused. The Highrise platform offers several tools that look similar but serve different purposes.
| What You Need | What to Use | Why |
|---|---|---|
| Live score during a match | Lua table or NetworkValue | Fast session state. NetworkValue can sync to clients for UI updates. |
| Official ranked leaderboard | Leaderboard API | Built for ranked entries and top-N retrieval. |
| Persistent custom player data | Storage API | Persists across sessions and world restarts. |
| Custom high-score with special rules | Storage with UpdateValue |
Prevents lost updates when the new value depends on the old one. |
| Display only | UXML + USS + Lua binding | Keeps presentation separate from score logic. |
The practical rule: if you need ranks, use the Leaderboard API. If you need custom persistent data that goes beyond simple scores, use Storage. If you need live UI updates during gameplay, use NetworkValue with its Changed event on the client.
For UI display, Highrise supports World UI (useful for in-world billboards or leaderboard displays), Above Chat, and HUD output modes. UXML defines the structure, USS handles styling, and Lua adds functionality through bindings.
This section is the difference between a leaderboard that works and one that gets exploited within hours.
-- BAD: client sends arbitrary score amount
AddScoreEvent:FireServer(999999)
The server receives a number from the client and blindly increments. Any player with basic tools can fire this event with any value they want. Practitioners on Reddit consistently flag this as the most common leaderboard vulnerability across game platforms, not just Highrise. One developer thread about online leaderboards highlighted that client-calculated scores are trivially easy to fake, making server validation a universal concern.
-- GOOD: client signals an action, not a score
CoinCollectedEvent:FireServer(coinId)
The server then checks whether that coin exists, whether it was already collected, whether the player is in a valid state, and only then awards a fixed number of points.
--!Type(Server)
local COIN_POINTS = 10
local collectedCoinsByPlayer = {}
local function canCollectCoin(player: Player, coinId: string)
collectedCoinsByPlayer[player.user.id] = collectedCoinsByPlayer[player.user.id] or {}
if collectedCoinsByPlayer[player.user.id][coinId] then
return false
end
-- Server-side checks:
-- Does this coin exist in the world?
-- Is the player close enough to collect it?
-- Is the game currently active?
collectedCoinsByPlayer[player.user.id][coinId] = true
return true
end
function AwardCoinIfValid(player: Player, coinId: string)
if not canCollectCoin(player, coinId) then
return
end
Leaderboard.IncrementScoreForPlayer("global_scores", player, COIN_POINTS, function(entry, error)
if error ~= LeaderboardError.None then
print("Leaderboard update failed: " .. tostring(error))
return
end
print(player.name .. " earned " .. COIN_POINTS .. " points.")
end)
end
Notice the use of player.user.id rather than player.id. The Storage docs clarify that player.user.id is a permanent unique identifier, while player.id is a temporary session ID. Using the wrong one means your tracking table resets every session.
Highrise Reddit discussions surface recurring complaints about perceived unfairness in competitive systems, including concerns about automation, alt accounts, and macro use. These are anecdotal community reports, not verified platform facts, but they point to something real: players care deeply about leaderboard integrity. A server-authoritative scoring pattern is the minimum standard.
If you’re building competitive experiences for the Highrise community, investing time in validation logic pays off in player trust.
The Leaderboard API is server-side only. There is no workaround. If your script has --!Type(Client) at the top, leaderboard methods will not work.
Every leaderboard call counts against a rate limit. Spamming IncrementScoreForPlayer on every frame or every tiny action will trigger RequestThrottled errors. Better approach: accumulate points in a local variable during active gameplay, then commit the score at checkpoints or match end.
The official testing example deletes a player’s leaderboard entry when they disconnect. That makes sense for temporary test sessions. It does not make sense for all-time or weekly high-score boards. Treat disconnect deletion as a testing pattern, not a production default.
Reset clears an entire leaderboard. It exists for testing and seasonal resets. Calling it accidentally in production destroys all player scores with no undo.
If you use Storage for custom high-score logic, the Storage docs warn that a get-then-set pattern can lose data when two players update the same value simultaneously. Use UpdateValue instead, which handles the prior-value dependency safely.
player.id is a temporary session identifier. player.user.id is the permanent unique ID. Using player.id as a storage key or tracking identifier means your data disappears when the session ends.
Lua community discussions, especially from creators learning similar platforms, consistently advise pairing documentation with small projects rather than watching tutorials passively. Practitioners on Reddit recommend actually building something small to internalize the concepts.
Here’s a progression for building your own Lua scripting example for a Highrise leaderboard system:
If you want to explore what other creators have built, browse Highrise Worlds to see leaderboard systems in action. For creator inspiration, check out Highrise Ideas or read through the latest updates on the Highrise blog.
Ready to start building? Download Highrise and set up your first World with Highrise Studio.
Score writes cannot run on the client. The Leaderboard API is server-side only, and Highrise security docs warn that client events can be manipulated. The client should only display results or signal actions for the server to validate.
Use IncrementScoreForPlayer when a player accumulates points over time, like collecting coins or completing tasks. Use SetScoreForPlayer when the score is an absolute result, like a final race time converted into points. Both are server-side Leaderboard API methods.
Call GetEntries with a starting rank of 1 and a limit of 10. Loop through the returned entries and display each one’s rank, name, and score in your UI. The entries come back sorted by rank automatically.
Not necessarily. Use the Leaderboard API when you need ranked scoreboards with built-in ranking. Use Storage when you need custom persistent data, complex progress tracking, or high-score logic that doesn’t fit a standard ranked board.
Keep score authority on the server. Let the client signal actions (like “I touched the coin”), but validate those actions server-side before awarding points. Never let the client specify the score amount. This follows Highrise’s official security guidance.
Only for temporary session-based boards. The official testing example uses disconnect deletion, but that pattern is wrong for all-time, weekly, or any persistent high-score leaderboard.
You get a RequestThrottled error. The LeaderboardError enum defines this as a rate-limit response. Batch your updates and avoid calling leaderboard methods every frame or on rapid-fire events.
player.user.id is the permanent unique identifier for a player across all sessions. player.id is a temporary session ID that changes every time the player joins. Always use player.user.id for storage keys, tracking tables, and anything that needs to persist.
© 2026 Pocket Worlds. All rights reserved.
Company