Roblox Leaderboards and OrderedDataStore: Building Rankings That Stay Accurate at Scale

Do you know what your global leaderboard actually shows a player thirty seconds after they set a personal best? If your score writes fire on every kill, pickup, and round end, the honest answer is that you do not, because a large share of those writes never reached the datastore at all.
Global leaderboards are the most-requested social feature in Roblox experiences and the one most consistently shipped broken. The sorting is the easy part.
The write budget, the read budget, and the true cost of computing a single player's rank are where boards drift away from reality. Every one of those is a decision you make before the first line of code, or pay for after a season of wrong ranks.
An OrderedDataStore is a DataStore that keeps integer values in sorted order and returns them in pages of up to 100 through GetSortedAsync. It handles ranking; it does not handle names, avatars, or any non-integer data.
What An OrderedDataStore Actually Gives You
You get an index, and only an index. Everything you want to render on a leaderboard row — display name, avatar thumbnail, clan tag, equipped cosmetic — has to live somewhere else and be joined at read time.
Before you design around it, be precise about what the API guarantees. The constraints that matter in production include but are not limited to:
- Integer values only. An OrderedDataStore rejects tables, strings, and booleans, so a score with decimals has to be scaled to an integer first. Storing race times in milliseconds rather than seconds, or currency in cents rather than dollars, is the usual fix.
- Sorted pages, not random access.
GetSortedAsync(ascending, pageSize, minValue, maxValue)hands back a DataStorePages object you walk withAdvanceToNextPageAsync. There is no call that answers "what rank is UserId 12345 at?" - No versioning and no key listing. Standard DataStores give you version history and key enumeration; the ordered variant gives you neither. If you overwrite or delete an entry incorrectly, there is nothing to roll back to.
- A separate request budget. Ordered writes and sorted reads draw from different pools than your save system, which is why leaderboard traffic can starve player saves without ever appearing in your save telemetry.
All of this adds up to one design rule: treat the ordered store as a ranking index over UserIds, and keep the display record in your standard save. If you have not settled that split yet, our guide to Roblox DataStore patterns and save architecture covers the session-locking and retry scaffolding this post assumes you already have in place.
Pick The Right Store For The Right Board
Most experiences need more than one leaderboard, and they do not all belong in the same service. A round-scoped board and an all-time board have almost nothing in common operationally.
| Service | Best for | Value type | Persistence | Sorted reads |
|---|---|---|---|---|
| OrderedDataStore | All-time and seasonal boards | Integers only | Durable | Yes, paged to 100 |
| Standard DataStore | Display records, saves, audit rows | Tables, strings, numbers | Durable, versioned | No |
| MemoryStoreService SortedMap | Live round, hourly, and event boards | Small values with a sort key | Ephemeral, TTL-bound | Yes, by range |
MemoryStore is the piece most teams skip, and it is usually the one that would have saved them. Its throughput is far higher than DataStore's and its entries expire on a TTL, which is exactly the shape of a board that only needs to be true for the next twenty minutes.
Why Leaderboard Writes Die Before They Land
DataStore write budgets are computed per server and refill each minute, and your leaderboard competes with every other system you run. A 30-player server has a set and increment allowance in the neighborhood of 60 plus 10 per player per minute, which sounds generous until a combat loop starts writing on every scoring event.
The second limit is sharper and far less discussed: the same key cannot accept writes more often than roughly once every six seconds. Two updates to one player's key inside that window means one of them queues behind the other, and a queue that keeps growing eventually errors instead of waiting.
Roblox throttles repeated writes to the same DataStore key to about one every six seconds. Score updates fired per gameplay event exceed that immediately, so the board lags behind live state and drops updates under load.
The failure mode is quiet, which is what makes it dangerous. Nothing crashes, no error reaches the player, and the board simply displays a number that was true four minutes ago.
Write The Score, Not The Event
The fix is to stop treating the datastore as the source of truth during a live session. The server holds the authoritative score in memory, marks it dirty on change, and flushes on a schedule.
A flush cadence of 45 to 60 seconds per player, plus a forced flush on PlayerRemoving and inside BindToClose, keeps a 30-player server comfortably inside budget. Add a dirty check so an idle player costs you nothing at all.
Use UpdateAsync with a monotonic transform rather than SetAsync or IncrementAsync. Returning math.max(oldValue or 0, newValue) means a stale server, a duplicate session, or a retried request can never lower a score or double it.
Retry safety: IncrementAsync is not idempotent, and a request that times out on the network may still have been applied server-side. Retrying it inflates the total, and that inflation is indistinguishable from cheating when you audit the board three weeks later.
Before a burst of writes, check DataStoreService:GetRequestBudgetForRequestType() and defer rather than firing blindly. Budget-aware flushing turns a hard throttle into a slightly delayed update, which no player will ever perceive.
Every one of these calls belongs inside a pcall with capped exponential backoff and jitter. The patterns for structuring that retry wrapper cleanly are covered in our breakdown of Luau scripting patterns for production systems.
Reading The Board Without Burning Your Read Budget
GetSortedAsync draws from a much smaller pool than ordinary reads — on the order of five plus two per player each minute. Calling it once per player who opens the leaderboard GUI will throttle a busy server within seconds.
Fetch the board once per server on a timer, cache the result, and replicate the cached payload to every client that asks. One refresh every 30 to 60 seconds is indistinguishable from live on an all-time board, and it costs one request instead of forty.
Call GetSortedAsync once per server on a 30 to 60 second timer, cache the page, and replicate that payload to clients. Never call it per player — the sorted-read budget is only about five plus two per player each minute.
Requesting a page size of 100 and rendering the top 10 is pure waste; request exactly what you display. If your UI shows a top 25, ask for 25 and skip pagination entirely.
Where the board has to feel live — an active round, an hourly event, a match result screen — read from a MemoryStore SortedMap and let the ordered store handle only the durable all-time view. Coordinating that state across servers is the same problem space as Roblox server architecture and cross-server coordination.
Finding One Player's Rank Is The Expensive Part
Here is the constraint nobody plans for: an OrderedDataStore can hand you the top N, but it cannot tell you that a specific UserId sits at position 41,203. Getting that number exactly would mean paging through 412 pages of results, which is impossible inside your read budget and pointless besides.
There are three honest ways out. The first is the one most experiences should ship.
- Exact rank inside the top N only. Fetch one page of 100, build a UserId-to-index map, and show a precise position to anyone in it. Everyone else sees a percentile or an unranked state.
- Percentile from a bucketed histogram. Maintain a second store holding counts per score band — 0 to 999, 1,000 to 4,999, and upward — refreshed on a slow cadence. A player's standing is the sum of counts in the bands above theirs, which is one cached read rather than full pagination.
- Threshold search with minValue and maxValue.
GetSortedAsyncaccepts bounds, so you can binary-search for the score sitting at a known rank and cache those cut points. That gives you honest "top 1%" and "top 10%" markers refreshed hourly for a handful of requests.
Percentiles also read better than raw positions. "Top 3% this season" motivates more than "#41,203", and it happens to be the number you can actually compute.
OrderedDataStore has no rank lookup by key. Show exact positions only for the top 100 from a single sorted page, and give everyone else a percentile derived from a cached score histogram.
Seasonal Resets Without A Wipe
Never clear a leaderboard in place. Roll the key namespace instead — Score_S7 becomes Score_S8 — and last season's data stays readable forever as both your history and your dispute-resolution record.
Derive the season identifier from a deterministic function of os.time() so every server agrees without any coordination message. A pure function from timestamp to key name is also trivially unit-testable, which matters when the boundary lands at 3am and nobody is watching.
The midnight bug: compute the season key at write time, not at server start. A server that booted an hour before the rollover will otherwise keep writing into the closed season for as long as it stays up, quietly corrupting both boards.
Reset a season by rolling the key name, not by deleting data. Derive the season suffix from os.time() at write time so a long-running server cannot keep writing into a closed season after the boundary passes.
Settlement — actually awarding the prizes — deserves its own idempotency key. Record payouts under a composite key of season and UserId, check it before granting, and a retried or duplicated settlement pass becomes a no-op instead of a double payout.
If prizes involve currency or premium items, the same discipline applies on the grant side. Our notes on Roblox monetization and reward economies go deeper on receipt handling and grant idempotency.
Keeping Ranks Honest When Exploiters Inflate Scores
A leaderboard is the highest-value target in your experience, because it converts cheating into public status. The first rule is absolute: no score value ever originates on the client.
Every increment must derive from server-validated state — a kill the server adjudicated, a checkpoint the server confirmed, a purchase the server processed. A RemoteEvent that carries a score delta is a scoreboard you have handed to the exploiter.
Score values must never originate on the client. Derive every point from server-adjudicated events, clamp each session to a plausible maximum earn rate, and route anything above that ceiling to a quarantine key for review.
Beyond authority, add plausibility bounds. If your theoretical maximum earn rate is 300 points per minute, a session producing 40,000 points in eight minutes is not a great player, and clamping that write costs you nothing legitimate.
- Rate clamps per session. Track points earned per rolling window on the server and cap the flush at the plausible ceiling. Log the overage rather than silently discarding it, so you keep the signal for review.
- A quarantine key. Suspicious totals write to a shadow ordered store instead of the live board and get promoted only after review. The visible board stays clean without you having to be right on the first pass.
- Audit rows in a standard DataStore. When a session crosses a threshold, write the event breakdown — sources, timestamps, deltas — to a normal DataStore keyed by session id. That record is what turns a suspicion into a defensible removal.
- Self-healing removal.
RemoveAsyncon the offending key drops that entry, and because ranks are derived from the sorted set rather than stored, the board repairs itself on the very next read.
Keep in mind that a leaderboard is a detector as much as a display. Anomalies at the top of a board are usually the earliest visible symptom of an exploit that is also draining your economy, which is why board monitoring belongs alongside the checks in our guide to Roblox anti-exploit and server-side validation.
One more identity concern: if players reach your experience from multiple platforms or link accounts, make sure the key you rank on is durable. Ranking on anything unstable produces duplicate rows for the same human, a problem covered in our guide to cross-platform player identity.
Rendering The Board Without Hitching The Client
Having solved the data problem, most teams then rebuild 100 GUI rows from scratch on every refresh. That allocation churn produces a visible hitch every single time the timer fires.
Pool the row frames once at startup, then update only text and image properties on refresh. A board that never instantiates a Frame after initialization costs effectively nothing per update.
Send the cached payload as one table over a single RemoteEvent rather than firing per row, and only to clients with the board actually open. The cost model here is the same one described in our guide to Roblox replication and network ownership.
Testing A Board That Only Breaks At Scale
A leaderboard holding twelve entries hides every pagination bug you will eventually ship. Seed a development universe with several thousand synthetic entries across a realistic score distribution, then read against that instead.
Use a distinct store name or scope for Studio and development builds so test writes never touch the live board. A mistaken write into production ordering is not reversible, because ordered stores keep no version history to restore from.
Instrument the flush path with four counters: writes attempted, writes throttled, retries, and seconds since the last successful flush per player. When the board looks wrong, those numbers tell you within seconds whether the problem is budget, logic, or a bad score source.
Frequently Asked Questions
Can I store negative scores in an OrderedDataStore?
Ordered stores are built around non-negative integers, and negative values behave unreliably in sorted reads. Offset your scale instead — store the score plus a fixed constant, then subtract that offset at display time.
Should the leaderboard live in the same DataStore as player saves?
No. Keep them in separate stores so a leaderboard read storm cannot starve the save path, and so you can change the leaderboard schema without touching save migration logic.
How do I handle a player whose score should decrease?
Decreases break the monotonic-max write pattern, so they need an explicit authority check and a distinct code path. Most experiences are better served by a rolling seasonal board than by ever lowering an all-time score.
What happens to the board during a DataStore outage?
Reads fail and your cached payload goes stale, which is acceptable if you display that cache with a timestamp. Writes should queue in memory behind a bounded buffer and flush when budget returns, and should never block gameplay.
Is it worth building a leaderboard on an external web service?
Only when you need queries the platform cannot answer — friend-scoped boards, arbitrary filters, or millions of exact ranks. That path adds HttpService latency, its own rate limits, and an availability dependency you now own.
Where To Take This Next
A leaderboard that stays accurate is mostly an exercise in restraint: fewer writes, cached reads, honest percentiles, and a hard line between client input and server truth. Every one of those decisions is far cheaper to make now than to retrofit after a season of drifted ranks.
If you are scoping ranking systems, seasonal events, or cross-server progression, the rest of our Roblox and game development guides cover the surrounding systems — data persistence, replication, matchmaking, and exploit defense — at the same level of detail.


