roblox-server-data
Use for Roblox server or cross-server data: OrderedDataStore leaderboards, MessagingService, world state, seasons, or guilds.
Install
npx skills add https://github.com/TabooHarmony/roblox-brain/tree/main/skills/core/roblox-server-data
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install tabooharmony-roblox-brain@llmmart
git clone https://github.com/TabooHarmony/roblox-brain.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole tabooharmony/roblox-brain collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Roblox Server & Shared Data
When to Load
Load for server-level or cross-server data: leaderboards (OrderedDataStore), cross-server messaging (MessagingService), queues and sorted maps (MemoryStoreService), shared world state, non-player persistence, season or guild data. For player data, use roblox-data; for Open Cloud, use roblox-cloud.
Quick Reference
OrderedDataStore (Leaderboards)
- Sortable DataStore. Keys are strings (
tostring(UserId)); values are integers used for sorting. GetSortedAsync(ascending, pageSize, minValue, maxValue)→ sorted pagesBatchGetAsync(keys): multi-key read, ordered stores only; missing keys are omitted- Budget: poll
GetRequestBudgetForRequestType(OrderedWrite)before bursts (per server: 30 + numPlayers x 5 writes/min) - For leaderboards only, never for player saves
- Keep the user ID a static substring in keys (
player_<UserId>) so RTBF templates match; hashed keys make erasure manual
MessagingService (Cross-Server)
SubscribeAsync/PublishAsync; no delivery or ordering guarantee, so design for idempotency.
GlobalDataStore (Shared State)
- Persistent non-player state (guilds, seasons, counters). Use
UpdateAsync; never for player session data.
MemoryStoreService (Temporary Coordination)
- Queues and sorted maps for expiring matchmaking, leases, coordination.
- Remove a read batch only after successful, idempotent processing.
Cross-Server Patterns
- Register servers with expiring heartbeats; use MessagingService for notifications.
User Identity
- New code identifies users with
player.User(User.Id,DomainType,DomainId);UserIdstays valid. Domain IDs are per-experience, so keep cross-server keys onUserIdand never mix the two.
Pitfalls
- MessagingService: fire-and-forget, unordered, cross-server latency; not for time-critical work
- GlobalDataStore: same rate limits as player DataStores
- Never store Instances; serialize to primitives first
SetAsyncoverwrites without reading; useUpdateAsyncfor shared counters
Need more detail? Load references/full.md for the complete reference with code examples, API tables, and edge cases.
Files (roblox-brain)
-
references
-
full.md 16.5 KB
# Roblox Server & Shared Data: Full Reference > **Code in this reference is illustrative. Adapt to your game and verify in Studio before production use.** ## OrderedDataStore ### Overview OrderedDataStore is a sortable variant of DataStore. Keys are strings, while values are integers used for ordering. Use a stable string key such as `tostring(UserId)` for player leaderboards. ### API | Method | Purpose | |--------|---------| | `GetSortedAsync(ascending, pageSize, minValue, maxValue)` | Get sorted pages of entries | | `SetAsync(key, value)` | Set a key's value (key is a string; value is an integer) | | `IncrementAsync(key, delta)` | Atomically increment a key's value | | `RemoveAsync(key)` | Remove an entry | | `UpdateAsync(key, transformFunction)` | Atomic read-modify-write | ### Leaderboard Implementation ```luau local DataStoreService = game:GetService("DataStoreService") local coinStore = DataStoreService:GetOrderedDataStore("LeaderboardCoins") -- Update player's score local function updateScore(userId: number, coins: number) local success, err = pcall(function() coinStore:SetAsync(tostring(userId), coins) end) if not success then warn("Leaderboard update failed:", err) end end -- Fetch top 10 local function getTopPlayers(count: number): {any} local success, pages = pcall(function() return coinStore:GetSortedAsync(false, count) end) if not success then return {} end return pages:GetCurrentPage() end -- Iterate all pages local function processAllEntries(callback) local pages = coinStore:GetSortedAsync(false, 100) while true do for _, entry in pages:GetCurrentPage() do callback(entry.key, entry.value) end if pages.IsFinished then break end pages:AdvanceToNextPageAsync() end end ``` ### Key Rules - Keys MUST be strings; use a stable representation such as `tostring(player.UserId)` - Values must be integers used for sorting; do not store strings, tables, or nested data - Separate from player DataStore (different key space, different purpose) - `GetSortedAsync` returns pages, not a flat list; use pagination - Rate limits apply same as regular DataStores ### Batch reads: `BatchGetAsync` `GlobalDataStore:BatchGetAsync(keys, options?)` retrieves multiple ordered-data-store keys in a single request. Per the docs it is currently only supported on `OrderedDataStore`; calling it on a standard `GlobalDataStore` or `DataStore` throws an error. Unlike `GetAsync`, it returns no `DataStoreKeyInfo`, because ordered stores do not support versioning or metadata. <!-- temporal: 2026-09 --> - Returns a dictionary mapping each key to a table with a `value` field (for example `coins = { value = 100 }`). - Keys that do not exist or have empty values are omitted from the result, not returned as nil. - The `keys` array needs at least one key, at most a server-configured maximum (default 100). Exceeding it throws. - Each call counts against the ordered read budget based on the number of keys requested. ```luau local keys = {"player_111", "player_222", "player_333"} local success, results = pcall(function() return coinStore:BatchGetAsync(keys) end) if success then for _, key in keys do local entry = results[key] if entry then print(key .. " : " .. tostring(entry.value)) else print(key .. " has no saved entry") end end end ``` ### Reading request budgets `DataStoreService:GetRequestBudgetForRequestType(requestType)` returns the number of requests the current place can still make for a `DataStoreRequestType` (for ordered stores: `OrderedRead`, `OrderedWrite`, `OrderedList`, `OrderedRemove`). Requests beyond the budget are throttled. Poll it before bursts instead of assuming quota headroom; ordered writes default to only 30 + numPlayers x 5 per minute per server. ```luau local function waitForBudget(requestType: Enum.DataStoreRequestType) while DataStoreService:GetRequestBudgetForRequestType(requestType) <= 0 do task.wait(1) end end waitForBudget(Enum.DataStoreRequestType.OrderedWrite) ``` Quota formulas (read, write, list, remove; experience-level and per-server) and the rest of the limits tables are in the `roblox-data` full reference, section Limits and quotas. ### Leaderboard Alternatives: Cached DataStore OrderedDataStore is the canonical primitive, but a leaderboard can instead be stored in a standard `DataStore` kept fully loaded in memory, with writes coalesced into batched `UpdateAsync` saves. This "cached board" model trades storage size and in-memory footprint for O(1) rank lookup of arbitrary players (OrderedDataStore only exposes ~100 entries/page) and lets you define custom ordering and non-integer/custom value types. Prefer it when arbitrary-player rank lookup, custom ranking, or API-request throttling (batching many queued saves into a few writes) outweigh a smaller, paginated board. Community leaderboard modules (e.g. Leaderboards+, https://devforum.roblox.com/t/leaderboards-the-ultimate-module-for-global-leaderboards/4706939) are a lead for this pattern. ## MessagingService ### Overview Real-time communication between server instances. Fire-and-forget: no delivery guarantee, no ordering guarantee. ### API | Method | Purpose | |--------|---------| | `SubscribeAsync(topic, callback)` | Listen for messages on a topic | | `PublishAsync(topic, message)` | Broadcast to all servers subscribed to topic | ### Server Registration Pattern ```luau local MessagingService = game:GetService("MessagingService") local jobId = game.JobId -- Announce this server is alive local function announceServer() local serverInfo = { jobId = jobId, playerCount = #game.Players:GetPlayers(), capacity = game.Players.MaxPlayers, } pcall(function() MessagingService:PublishAsync("ServerHeartbeat", serverInfo) end) end -- Listen for heartbeats from other servers local activeServers = {} MessagingService:SubscribeAsync("ServerHeartbeat", function(message) local data = message.Data activeServers[data.jobId] = { playerCount = data.playerCount, lastSeen = os.time(), } end) -- Heartbeat loop task.spawn(function() while true do announceServer() -- Clean stale servers (no heartbeat in 30s) for id, info in activeServers do if os.time() - info.lastSeen > 30 then activeServers[id] = nil end end task.wait(10) end end) ``` ### Cross-Server Player Migration ```luau -- When a player leaves, notify other servers to release any shared locks game.Players.PlayerRemoving:Connect(function(player) pcall(function() MessagingService:PublishAsync("PlayerLeft", { userId = player.UserId, fromServer = jobId, }) end) end) ``` ### Key Rules - Messages are fire-and-forget; design for idempotency - No ordering guarantee; handle out-of-order messages gracefully - Rate limit: 600 + 240 * (players in server) messages per minute - Message size limited; keep payloads small - `message.Data` contains the published payload - Subscribe callbacks run in a separate thread; wrap them in pcall ## GlobalDataStore ### Overview GlobalDataStore is the same DataStore API but used for shared, non-player state. No session locking. Use `UpdateAsync` for atomic operations on shared counters. ### Guild/Clan Data Pattern ```luau local DataStoreService = game:GetService("DataStoreService") local guildStore = DataStoreService:GetDataStore("GuildData") -- Tagged outcome so failure and absence stay distinct: -- (true, data) -> read succeeded, guild exists -- (true, nil) -> read succeeded, guild not created yet (legitimate nil) -- (false, err) -> DataStore outage; do NOT treat this as "no guild" -- Never silently return nil on error. local function getGuild(guildId: string): (boolean, any) return pcall(function() return guildStore:GetAsync("guild_" .. guildId) end) end local ok, data = getGuild("guild123") if not ok then -- DataStore failure: retry, back off, or serve stale cache. Do not create/overwrite. elseif data then print("guild name", data.name) end local function updateGuild(guildId: string, callback: (data: table) -> table) local key = "guild_" .. guildId local success, err = pcall(function() guildStore:UpdateAsync(key, function(oldData) local data = oldData or { members = {}, createdAt = os.time() } return callback(data) end) end) if not success then warn("Guild update failed:", err) end end -- Add member atomically updateGuild("guild123", function(data) table.insert(data.members, { userId = 456, role = "member", joinedAt = os.time() }) return data end) ``` ### Atomic Counter Pattern ```luau local counterStore = DataStoreService:GetDataStore("GlobalCounters") -- Tagged outcome: returns (false, err) instead of just failing silently; the -- caller decides whether to retry, queue, or alert. Errors are preserved, not swallowed. local function incrementCounter(name: string, delta: number): (boolean, any) local ok, err = pcall(function() counterStore:UpdateAsync("counter_" .. name, function(oldValue) return (oldValue or 0) + delta end) end) if not ok then warn(("counter increment failed for %s: %s"):format(name, tostring(err))) end return ok, err end ``` ### Key Rules - Always use `UpdateAsync` for shared state: `SetAsync` can lose updates - Never store Instances; serialize to primitives - Same rate limits as player DataStores - No session locking, so don't use it for player data - Key naming: use prefixes to namespace (`guild_`, `counter_`, `season_`) - Read helpers must preserve failure information: tag outcomes (ok/error/missing) or return `(success, data-or-err)`; never collapse an outage into nil, and never synthesize authoritative-looking fallback data on a failed read - Cross-owner atomicity: per-key atomicity does not span profiles or services; for the two-profile exchange limit, quiescence, and recovery patterns, see `roblox-data` full reference, section Cross-owner atomicity limits ## MemoryStoreService MemoryStore is for temporary, high-throughput coordination. Its values expire, so it is a poor place for authoritative player progress or a permanent leaderboard. ### Queue pattern Processor contract: `processMatchRequest(request)` returns `true` only when the batch's durable effect has fully succeeded, and returns `false` or throws when it has not. `drain` removes the read batch only when every request returned `true`. If any request fails, the whole batch stays queued; redelivery must be safe, so processing stays idempotent and includes a stable request identifier. ```luau local MemoryStoreService = game:GetService("MemoryStoreService") local queue = MemoryStoreService:GetQueue("Matchmaking", 30) local function enqueue(request: table) return pcall(function() queue:AddAsync(request, 60, 0) end) end -- Contract: processMatchRequest(request) returns true on success. -- Returning false or throwing means "did not complete"; drain then -- keeps the batch queued instead of removing it. local function drain(maxItems: number): (boolean, any) local ok, items, readId = pcall(function() return queue:ReadAsync(maxItems, false, 0) end) if not ok then -- items holds the error message here; propagate it instead of -- returning silently and losing the failure. return false, items end local allSucceeded = true for _, request in items do -- Capture BOTH pcall's ok AND the processor's return value: the -- contract is "returns true on success", so a false return is a -- failed batch too. Capturing only pcall's first result lets a -- false-returning processor have its batch removed anyway. local requestOk, processed = pcall(function() return processMatchRequest(request) end) if not requestOk or processed ~= true then allSucceeded = false end end -- Remove only when every request durably succeeded. A failed batch is -- left queued (invisible until the timeout expires) and must be safe -- to redeliver: process idempotently. if allSucceeded and #items > 0 then local removed = pcall(function() queue:RemoveAsync(readId) end) if not removed then return false, "batch processed but RemoveAsync failed; it will be redelivered" end elseif not allSucceeded then return false, "one or more requests failed; batch left queued" end return true end ``` `ReadAsync` makes items temporarily invisible to other readers. A crashed worker can therefore leave work to reappear later. Make processing idempotent, include a request or match identifier, and remove an item only after the durable or teleport-side effect has succeeded. Keep the invisibility timeout long enough for the work but short enough to recover from a dead server. ### Sorted-map pattern Use a sorted map for short-lived records keyed by a stable identifier, such as server heartbeats or matchmaking candidates. Set an expiration on every entry and clean stale values when reading. Do not confuse a sorted map with `OrderedDataStore`: MemoryStore entries expire and are intended for coordination, not historical persistence. The recent `PartyService Plus` DevForum resource is a useful concept specimen for queue cleanup, party leadership transfer, and cross-server matchmaking. It is a commercial community resource, not a canonical API reference. The official MemoryStore API remains the source of truth. ## Persistent World State ### Building/Construction Games Serialize player-built structures as tables, store per-player or globally: ```luau -- Serialize a built structure local function serializeBuild(building: Model): table local parts = {} for _, part in building:GetDescendants() do if part:IsA("BasePart") then table.insert(parts, { class = part.ClassName, cf = {part.CFrame:GetComponents()}, size = {part.Size.X, part.Size.Y, part.Size.Z}, color = {part.Color.R, part.Color.G, part.Color.B}, material = part.Material.Name, }) end end return { name = building.Name, parts = parts } end ``` ### Season/Event Data ```luau local seasonStore = DataStoreService:GetDataStore("SeasonData") -- Tagged outcome: (true, data) on a successful read, (false, err) on failure. -- A successful read with nil data means no season has been written yet. -- Never synthesize a fallback season here: fabricated data ({ season = 1, ... }) -- gets consumed as authoritative and silently corrupts rewards and schedules. local function getSeasonInfo(): (boolean, any) return pcall(function() return seasonStore:GetAsync("current_season") end) end local ok, season = getSeasonInfo() if not ok then -- Read failed: retry, serve a cached copy, or fail the request. -- Do not fall back to invented season state. elseif season then -- Normal path: use the stored season data. else -- Read succeeded but no season exists yet: bootstrap the first season -- with an explicit write, don't fabricate one for this request. end ``` ## Pitfalls - **MessagingService delivery**: not guaranteed. If a server misses a message, it's gone. Design for eventual consistency. - **GlobalDataStore rate limits**: same as player DataStores. Don't use for high-frequency updates. - **OrderedDataStore key type**: must be a string, such as `tostring(UserId)`. Values must be integers used for sorting. - **Lost updates with SetAsync**: always use `UpdateAsync` for shared state. `SetAsync` overwrites without reading. - **Serialization**: DataStores only store JSON-compatible types (string, number, boolean, table, nil). No Instances, no functions, no userdata. - **Cross-server timing**: MessagingService has latency. Don't rely on it for time-critical operations. - **RTBF template matching**: leaderboard keys that embed the user ID as a static string (for example `player_<UserId>`) can be matched by automated right-to-be-forgotten deletion templates; hashed or random keys cannot, making erasure manual. See the `roblox-data` full reference, section Right-to-be-forgotten deletion templates. ## User identity in shared data Keep leaderboard keys and cross-server payloads on a documented, consistent identity scheme; don't mix `UserId`-based keys with domain-scoped `User.Id` keys. For the identity distinction, serialization, and migration implications, see `roblox-data` §1a.
-
-
SKILL.md 2.8 KB
--- name: roblox-server-data description: "Use for Roblox server or cross-server data: OrderedDataStore leaderboards, MessagingService, world state, seasons, or guilds." last_reviewed: 2026-09-13 sources: - https://create.roblox.com/docs/reference/engine/classes/OrderedDataStore - https://create.roblox.com/docs/reference/engine/classes/MessagingService - https://create.roblox.com/docs/reference/engine/classes/MemoryStoreService - https://create.roblox.com/docs/reference/engine/classes/DataStoreService - https://create.roblox.com/docs/cloud-services/data-stores/right-to-be-forgotten - https://create.roblox.com/docs/cloud-services/data-stores/error-codes-and-limits --- # Roblox Server & Shared Data ## When to Load Load for server-level or cross-server data: leaderboards (OrderedDataStore), cross-server messaging (MessagingService), queues and sorted maps (MemoryStoreService), shared world state, non-player persistence, season or guild data. For player data, use `roblox-data`; for Open Cloud, use `roblox-cloud`. ## Quick Reference ### OrderedDataStore (Leaderboards) - Sortable DataStore. Keys are strings (`tostring(UserId)`); values are integers used for sorting. - `GetSortedAsync(ascending, pageSize, minValue, maxValue)` → sorted pages - `BatchGetAsync(keys)`: multi-key read, ordered stores only; missing keys are omitted - Budget: poll `GetRequestBudgetForRequestType(OrderedWrite)` before bursts (per server: 30 + numPlayers x 5 writes/min) - For leaderboards only, never for player saves - Keep the user ID a static substring in keys (`player_<UserId>`) so RTBF templates match; hashed keys make erasure manual ### MessagingService (Cross-Server) - `SubscribeAsync` / `PublishAsync`; no delivery or ordering guarantee, so design for idempotency. ### GlobalDataStore (Shared State) - Persistent non-player state (guilds, seasons, counters). Use `UpdateAsync`; never for player session data. ### MemoryStoreService (Temporary Coordination) - Queues and sorted maps for expiring matchmaking, leases, coordination. - Remove a read batch only after successful, idempotent processing. ### Cross-Server Patterns - Register servers with expiring heartbeats; use MessagingService for notifications. ### User Identity - New code identifies users with `player.User` (`User.Id`, `DomainType`, `DomainId`); `UserId` stays valid. Domain IDs are per-experience, so keep cross-server keys on `UserId` and never mix the two. ### Pitfalls - MessagingService: fire-and-forget, unordered, cross-server latency; not for time-critical work - GlobalDataStore: same rate limits as player DataStores - Never store Instances; serialize to primitives first - `SetAsync` overwrites without reading; use `UpdateAsync` for shared counters **Need more detail?** Load `references/full.md` for the complete reference with code examples, API tables, and edge cases.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.