Specification
- Skill ID
- gamedev-skills/awesome-gamedev-agent-skills/roblox-datastores
- Publisher
- gamedev-skills
- Repository
- awesome-gamedev-agent-skills
- Installs
- 0
- Files
- 2
- Synced
- Sep 23, 2026
How to use it
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
gamedev-skills/awesome-gamedev-agent-skills/roblox-datastoresInstalls these files- SKILL.md
- references/sessions-and-limits.md
What this skill tells the agent
Roblox DataStores
Persist data across sessions in Roblox with DataStoreService: loading on join, saving on leave and shutdown, safe updates, retries, and ordered stores for leaderboards. Server-side only.
When to use
- Use to save/load player progress (coins, inventory, levels), build persistent leaderboards, or fix data loss, overwrites, and throttling.
- Use when server code calls
DataStoreService,GetDataStore,GetAsync,SetAsync,UpdateAsync, orGetOrderedDataStore.
*When not to use:* general scripting, services, remotes, the client/server split → roblox-luau. High-frequency temporary state (matchmaking, per-round) → memory stores (a different service). Engine-agnostic persistence theory → save-systems.
Core workflow
- Enable Studio access once. File → Game Settings → Security → Enable Studio Access to API Services (use a test place; Studio hits live data). DataStores work only from server
Scripts, neverLocalScripts. - Get a store, then read/write by key.
DataStoreService:GetDataStore("Name"); key per player is usually"Player_" .. player.UserId. - Wrap every call in `pcall`.
GetAsync/SetAsync/UpdateAsyncare network calls that can fail; an unguarded failure errors the thread and risks data loss. - Load on `PlayerAdded`, save on `PlayerRemoving`, and also `BindToClose`. A leaving player and a shutting-down server both need a final save.
- Prefer `UpdateAsync` for read-modify-write (multi-server safe) over
SetAsync(blind overwrite). On a failed load, do not overwrite with defaults — abort the save so you don't wipe good data. - Use `OrderedDataStore` for ranked data (leaderboards) via
GetSortedAsync. Test by joining, changing data, rejoining, and confirming it persisted.
Patterns
1. Load on join (pcall-guarded)
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local store = DataStoreService:GetDataStore("PlayerData")
local DEFAULT = { Coins = 0, Level = 1 }
Players.PlayerAdded:Connect(function(player)
local key = "Player_" .. player.UserId
local ok, data = pcall(function()
return store:GetAsync(key)
end)
if not ok then
-- Load FAILED (network). Do not treat as a new player; flag so we never save
-- over their real data with defaults.
warn("Load failed for", player.Name, data)
player:SetAttribute("DataLoaded", false)
return
end
player:SetAttribute("DataLoaded", true)
local profile = data or DEFAULT -- nil == genuinely new player
applyToLeaderstats(player, profile)
end)2. Save with UpdateAsync (multi-server safe)
-- UpdateAsync reads the latest value, then writes what the callback returns.
-- The callback MUST NOT yield (no task.wait, no further Async calls inside it).
local function savePlayer(player)
if player:GetAttribute("DataLoaded") == false then return end -- never overwrite on a bad load
local key = "Player_" .. player.UserId
local newData = gatherDataFor(player) -- a plain table of serializable values
local ok, err = pcall(function()
store:UpdateAsync(key, function(old)
-- merge/decide here; return nil to cancel the write
return newData
end)
end)
if not ok then warn("Save failed for", player.Name, err) end
end3. Save on leave AND on shutdown
Players.PlayerRemoving:Connect(savePlayer)
-- BindToClose runs when the server shuts down; save everyone still in.
-- It has a limited time budget, so save in parallel and yield until done.
game:BindToClose(function()
local players = Players:GetPlayers()
local remaining = #players
if remaining == 0 then return end
for _, player in players do
task.spawn(function()
savePlayer(player)
remaining -= 1
end)
end
while remaining > 0 do task.wait() end
end)4. Retry with backoff (transient failures)
local function withRetry(fn, attempts)
attempts = attempts or 3
for i = 1, attempts do
local ok, result = pcall(fn)
if ok then return true, result end
if i < attempts then task.wait(2 ^ i) end -- 2s, 4s, ... backoff
end
return false
end
local ok, data = withRetry(function() return store:GetAsync(key) end)5. Increment a counter
-- IncrementAsync is a convenience for integer read-modify-write (still wrap it).
local ok, newTotal = pcall(function()
return store:IncrementAsync("Visits_" .. player.UserId, 1)
end)6. Leaderboard with OrderedDataStore
local boards = DataStoreService:GetOrderedDataStore("Coins")
-- Write a player's score (call when it changes, not every frame).
pcall(function() boards:SetAsync("Player_" .. player.UserId, coins) end)
-- Read the top 10, descending.
local ok, pages = pcall(function()
return boards:GetSortedAsync(false, 10) -- ascending=false → highest first
end)
if ok then
for rank, entry in ipairs(pages:GetCurrentPage()) do
print(rank, entry.key, entry.value) -- entry.value is the number
end
endPitfalls
- Unhandled failure wipes progress → always
pcallAsync calls; on a failed load, mark the session and refuse to save so defaults never overwrite real data. - `SetAsync` race between servers → two servers writing the same key can clobber each other. Use
UpdateAsyncfor read-modify-write so each write sees the latest. - Yielding inside the `UpdateAsync` callback → the callback can't call
task.waitor other Async functions; compute the new value beforehand and return it. - No `BindToClose` save → players in the server at shutdown lose unsaved progress; add
game:BindToCloseand wait for saves to finish within its budget. - Throttling / "too many requests" → respect per-key and per-minute limits; don't save on every value change. Batch and save on a timer / on leave.
GetAsyncis cached briefly, so immediate re-reads may be stale. - Storing non-serializable values → only JSON-serializable data persists: numbers, strings, booleans, and tables with string/number keys.
Instances,Vector3,CFrame, and functions do not — serialize them to plain tables first. - Testing without API access → DataStores silently can't be used in Studio until Enable Studio Access to API Services is on (and they don't work from a
LocalScript). - `DataStoreKeyInfo` is nil for ordered stores →
OrderedDataStoredoesn't support versioning/metadata; use a regularDataStorewhen you need those.
References
- For session locking (preventing duplicate data across servers), versioning/ metadata with
DataStoreSetOptions, ordered-store pagination (AdvanceToNextPageAsync), the key error codes and request limits, and Right-to-be-Forgotten compliance, readreferences/sessions-and-limits.md.
Related skills
roblox-luau— services, instances, events, and the server/client model.save-systems— engine-agnostic serialization, slots, and migration.
More from gamedev-skills
Use this skill in a project
Open any RiverX project, search the Skills panel for gamedev-skills/awesome-gamedev-agent-skills/roblox-datastores, and install it in one click.
