← Back to list
📊

roblox-datastores Checked

Persist player data in Roblox with DataStoreService: GetDataStore, GetAsync/ SetAsync/UpdateAsync…
📊 Data & Analytics skills By gamedev-skills Version v1.0.0 Added 2026-10-21 Updated 2026-10-21 Source ↗ Part of Game Development →
9.0Overall rating
0Views
0Downloads
2.0kStars

Installation

🤖 Install via AI

Copy the prompt below and send it to your AI assistant (e.g. Claude Code) — it will follow the instructions and install automatically.

Install the "roblox-datastores" skill by following the instructions at https://skill123.me/install/roblox-datastores.
⌨️ Command line install

Run in your terminal — downloads and installs to ~/.claude/skills/.

curl -fsSL https://skill123.me/install/roblox-datastores.sh | bash
📦 Download ZIP

Download the zip and extract it into your skills directory (e.g. ~/.claude/skills/), then restart your session.

⬇ Download v1.0.0 · 6 KB
📊 Static rating
9.0/10
Trigger
9.0
Structure
0.0
Workflow
9.0
Content
0.0
Engineering
0.0
Security
9.0

Six-dimension rubric with reviewer rationale.

View scorecard ↓
🛡 Dynamic test
—

Not live-tested yet — this skill is rated on static analysis only.

About this skill

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.

Documentation

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, or GetOrderedDataStore.

**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

1. **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, never LocalScripts.

2. **Get a store, then read/write by key.** DataStoreService:GetDataStore("Name");

key per player is usually "Player_" .. player.UserId.

3. **Wrap every call in pcall.** GetAsync/SetAsync/UpdateAsync are network

calls that can fail; an unguarded failure errors the thread and risks data loss.

4. **Load on PlayerAdded, save on PlayerRemoving, and also BindToClose.** A

leaving player and a shutting-down server both need a final save.

5. **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.

6. **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
View full documentation

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 `pcall` Async 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 `UpdateAsync` for read-modify-write so each write sees the latest.
- **Yielding inside the `UpdateAsync` callback** → the callback can't call
  `task.wait` or 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:BindToClose` and 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. `GetAsync` is
  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. `Instance`s, `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** → `OrderedDataStore` doesn't
  support versioning/metadata; use a regular `DataStore` when 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, read `references/sessions-and-limits.md`.

## Related skills

- `roblox-luau` — services, instances, events, and the server/client model.
- `save-systems` — engine-agnostic serialization, slots, and migration.

Score breakdown

Trigger
9.0
Structure
0.0
Workflow
9.0
Content
0.0
Engineering
0.0
Security
9.0