roblox-datastores
gamedev-skills/awesome-gamedev-agent-skills
Persist player data in Roblox with DataStoreService: load on join, save on leave, safe updates, and leaderboards.
What is roblox-datastores?
DataStoreService persists player progress across sessions in Roblox experiences. Use it to save and load player data (coins, inventory, levels), build leaderboards, and prevent data loss through proper error handling, retries, and shutdown saves. Server-side only.
- Load player data on join and save on leave with pcall-guarded GetAsync/SetAsync/UpdateAsync calls
- Use UpdateAsync for multi-server-safe read-modify-write operations to prevent race conditions
- Build ranked leaderboards with OrderedDataStore and GetSortedAsync
- Implement retry logic with exponential backoff for transient network failures
- Save all players on server shutdown using BindToClose with parallel task spawning
- Increment counters safely with IncrementAsync for visit counts and similar metrics
How to install roblox-datastores
npx skills add https://github.com/gamedev-skills/awesome-gamedev-agent-skills --skill roblox-datastores- Enable Studio Access to API Services in Game Settings → Security (for testing in Studio)
- Server Script context (DataStores do not work in LocalScripts or client code)
- Basic understanding of Luau and Roblox services (see roblox-luau skill for fundamentals)
How to use roblox-datastores
- 1.Enable Studio access once in File → Game Settings → Security → Enable Studio Access to API Services
- 2.Get a DataStore with DataStoreService:GetDataStore("StoreName") in a server Script
- 3.Connect to PlayerAdded and wrap GetAsync in pcall to load data on join, marking failed loads
- 4.Connect to PlayerRemoving and use UpdateAsync in pcall to save data when players leave
- 5.Add game:BindToClose with parallel task.spawn saves to persist data during server shutdown
- 6.Use OrderedDataStore:GetSortedAsync for leaderboards and IncrementAsync for counters, both wrapped in pcall
Use cases
- Save player progression (coins, levels, inventory) and restore it when they rejoin
- Create a ranked leaderboard showing top players by coins or score across all servers
- Prevent data loss during server shutdowns by saving all active players before closing
- Implement a visit counter that increments safely across multiple concurrent servers
- Handle multi-server scenarios where two servers might write the same player key simultaneously
- Roblox game developers building persistent multiplayer experiences
- Backend scripters implementing player progression systems
- Game designers creating leaderboards and ranked competitions
- Developers migrating from broken save systems or fixing data loss issues
roblox-datastores FAQ
GetAsync, SetAsync, and UpdateAsync are network calls that can fail due to throttling, timeouts, or service issues. An unguarded failure crashes the thread and risks overwriting good data with defaults. pcall catches the error so you can handle it safely.
Use UpdateAsync for read-modify-write operations (safer on multi-server games) because it reads the latest value before writing. Use SetAsync only for simple overwrites. Never use SetAsync when multiple servers might write the same key.
No. Mark the session as not loaded and refuse to save until a successful load. Saving defaults after a failed load will overwrite the player's real data on the server. Only save if the load succeeded.
No. The callback must not yield; compute the new data beforehand outside the callback and return it immediately. Yielding inside breaks the atomic read-modify-write guarantee.
Use game:BindToClose to save all remaining players before the server closes. Spawn each save in a separate task so they run in parallel, then yield until all are done within the shutdown time budget.
Full instructions (SKILL.md)
Source of truth, from gamedev-skills/awesome-gamedev-agent-skills.
name: roblox-datastores description: > Persist player data in Roblox with DataStoreService: GetDataStore, GetAsync/ SetAsync/UpdateAsync/IncrementAsync wrapped in pcall, load-on-join and save-on-leave plus BindToClose, retries, and OrderedDataStore leaderboards. Use when saving or loading persistent data in a Roblox experience — when the user mentions DataStore, DataStoreService, GetAsync, SetAsync, UpdateAsync, save player data, or leaderboards. For general Luau scripting use roblox-luau.
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 onPlayerRemoving, and alsoBindToClose. A leaving player and a shutting-down server both need a final save. - Prefer
UpdateAsyncfor read-modify-write (multi-server safe) overSetAsync(blind overwrite). On a failed load, do not overwrite with defaults — abort the save so you don't wipe good data. - Use
OrderedDataStorefor ranked data (leaderboards) viaGetSortedAsync. 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
end
3. 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
end
Pitfalls
- Unhandled failure wipes progress → always
pcallAsync calls; on a failed load, mark the session and refuse to save so defaults never overwrite real data. SetAsyncrace between servers → two servers writing the same key can clobber each other. UseUpdateAsyncfor read-modify-write so each write sees the latest.- Yielding inside the
UpdateAsynccallback → the callback can't calltask.waitor other Async functions; compute the new value beforehand and return it. - No
BindToClosesave → players in the server at shutdown lose unsaved progress; addgame: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). DataStoreKeyInfois 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.
Related skills
More from gamedev-skills/awesome-gamedev-agent-skills and the wider catalog.

roblox-luau
Script Roblox experiences in Luau: services, instances, events, and secure client/server communication.

roguelike
Build turn-based grid dungeons with procedural generation, permadeath, and field-of-view.

router
Routes game-dev requests to the right specialized skill based on engine and task.

rpg
Build RPGs with stats, leveling, inventory, quests, dialogue, and combat systems.

save-systems
Design crash-safe, versioned save/load for game state across sessions and patches.

shader-programming
Write portable game shaders—vertex/fragment pipeline, coordinate spaces, UV math, and common effects (dissolve, outline, rim light) in GLSL with HLSL equivalents.