Loading...
Loading...
Persistence and cross-server data - DataStoreService (UpdateAsync, retries, budgets, limits), session locking with ProfileStore, schema migrations, OrderedDataStore leaderboards, MemoryStore queues/sorted maps/hash maps, MessagingService, right-to-be-forgotten. Use when saving player data, building leaderboards or queues, or fixing data loss, duplication, or throttling.
npx skill4agent add el4cteo/roblox-skills roblox-data-stores| Need | Service | Notes |
|---|---|---|
| Player progress, inventory, settings | DataStore via ProfileStore (session-locked) | One key per player ( |
| Global leaderboards | | Integer values only; periodic updates, cached reads. |
| Fast, temporary, cross-server state | | Sorted maps, queues, hash maps; everything expires (TTL ≤ 45 days). |
| Matchmaking queues, live events, server lists | MemoryStore queue / sorted map | Not durable — never the only copy of valuable data. |
| Broadcast to all servers | | ≤ 1 kB per message, best effort, no delivery guarantee. |
| Values changed by live-ops without publishing | Experience configs ( | See |
BindToCloseServerScriptService-- ServerScriptService/Server/Services/DataService.luau (ProfileStore)
local Players = game:GetService("Players")
local ServerScriptService = game:GetService("ServerScriptService")
local ProfileStore = require(ServerScriptService.ServerPackages.ProfileStore)
local TEMPLATE = {
version = 1,
coins = 0,
inventory = {} :: { [string]: number },
}
type PlayerData = typeof(TEMPLATE)
local PlayerStore = ProfileStore.New("PlayerData", TEMPLATE)
local profiles: { [Player]: typeof(PlayerStore:StartSessionAsync("")) } = {}
local function onPlayerAdded(player: Player)
local profile = PlayerStore:StartSessionAsync(`User_{player.UserId}`, {
Cancel = function()
return player.Parent ~= Players -- stop waiting if the player left
end,
})
if profile == nil then
player:Kick("Could not load your data. Please rejoin.")
return
end
profile:AddUserId(player.UserId) -- GDPR / right-to-be-forgotten linkage
profile:Reconcile() -- fill in fields added to TEMPLATE since this profile was created
profile.OnSessionEnd:Connect(function()
profiles[player] = nil
player:Kick("Your data was loaded on another server. Please rejoin.")
end)
if player.Parent == Players then
profiles[player] = profile
else
profile:EndSession() -- left while loading
end
end
for _, player in Players:GetPlayers() do
task.spawn(onPlayerAdded, player)
end
Players.PlayerAdded:Connect(onPlayerAdded)
Players.PlayerRemoving:Connect(function(player)
local profile = profiles[player]
if profile then
profile:EndSession() -- saves and releases the lock
end
end)
local DataService = {}
function DataService.get(player: Player): PlayerData?
local profile = profiles[player]
return profile and profile.Data
end
return DataServiceprofile.DataSetAsyncMessageAsyncgame:BindToCloseUpdateAsyncSetAsyncnilInstanceVector3CFramebuffer{ x, y, z }User_{UserId}| Limit | Value |
|---|---|
| Experience-wide standard reads / writes | |
| Default per-server reads/writes | |
| Ordered writes per server | |
| Per-key throughput | 25 MB/min read, 4 MB/min write |
| Storage (latest versions, compressed) | |
| Queue per request type | 30 pending requests, then errors 301–306 |
UpdateAsyncDataStoreService:GetRequestBudgetForRequestType(Enum.DataStoreRequestType.StandardWrite)SetRateLimitForRequestTypeListVersionsAsyncGetVersionAtTimeAsyncversion--!strict
type Data = { [string]: any }
local MIGRATIONS: { (Data) -> () } = {
[1] = function(data) -- v1 -> v2: "gold" renamed to "coins"
data.coins = data.gold or 0
data.gold = nil
end,
[2] = function(data) -- v2 -> v3: inventory list -> counts
local counts: { [string]: number } = {}
for _, itemId in data.inventory or {} do
counts[itemId] = (counts[itemId] or 0) + 1
end
data.inventory = counts
end,
}
local CURRENT_VERSION = #MIGRATIONS + 1
local function migrate(data: Data): Data
local version = data.version or 1
while version < CURRENT_VERSION do
MIGRATIONS[version](data)
version += 1
end
data.version = CURRENT_VERSION
return data
end
print(migrate({ gold = 5, inventory = { "Sword", "Sword" } }))GetSortedAsync(false, 100)Wins_2026W3964 KB + 1.2 KB × users1000 + 120 × CCUMemoryStoreSortedMapSetAsyncGetRangeAsyncUpdateAsyncMemoryStoreQueueAddAsyncReadAsyncRemoveAsyncMemoryStoreHashMapSetAsyncGetAsyncUpdateAsyncListItemsAsyncpcall600 + 240 × players20 + 8 × playersSubscribeAsyncPublishAsyncpcallPlayerData/User_{userId}roblox-open-cloudSetAsync(key, value, { userId })UpdateAsyncAddUserIdroblox-securityroblox-monetizationroblox-open-cloudroblox-teleport-matchmaking