Claude Skill

roblox-server-data

Use for Roblox server or cross-server data: OrderedDataStore leaderboards, MessagingService, world state, seasons, or guilds.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download tabooharmony-roblox-brain-skills_core_roblox-server-data-6051c35.zip · 7 KB
Part of tabooharmony/roblox-brain — 29 skills

Install

skills CLI npx skills add https://github.com/TabooHarmony/roblox-brain/tree/main/skills/core/roblox-server-data
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install tabooharmony-roblox-brain@llmmart
Git 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 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.

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.

No comments yet.

Reviews (0)

No reviews yet.

Related