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
- 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.
- 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/UpdateAsync are 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
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.
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 → 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.
1---2name: roblox-datastores3description: 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.4---5
6# Roblox DataStores
7
8Persist data across sessions in Roblox with `DataStoreService`: loading on join,
9saving on leave and shutdown, safe updates, retries, and ordered stores for
10leaderboards. Server-side only.
11
12## When to use
13
14- Use to save/load player progress (coins, inventory, levels), build persistent
15 leaderboards, or fix data loss, overwrites, and throttling.
16- Use when server code calls `DataStoreService`, `GetDataStore`, `GetAsync`,
17 `SetAsync`, `UpdateAsync`, or `GetOrderedDataStore`.
18
19**When *not* to use:** general scripting, services, remotes, the client/server
20split → `roblox-luau`. High-frequency temporary state (matchmaking, per-round) →
21memory stores (a different service). Engine-agnostic persistence theory →
22`save-systems`.
23
24## Core workflow
25
261. **Enable Studio access once.** File → Game Settings → Security → *Enable Studio
27 Access to API Services* (use a test place; Studio hits live data). DataStores
28 work only from server `Script`s, never `LocalScript`s.
292. **Get a store, then read/write by key.** `DataStoreService:GetDataStore("Name")`;
30 key per player is usually `"Player_" .. player.UserId`.
313. **Wrap every call in `pcall`.** `GetAsync`/`SetAsync`/`UpdateAsync` are network
32 calls that can fail; an unguarded failure errors the thread and risks data loss.
334. **Load on `PlayerAdded`, save on `PlayerRemoving`, and also `BindToClose`.** A
34 leaving player and a shutting-down server both need a final save.
355. **Prefer `UpdateAsync` for read-modify-write** (multi-server safe) over `SetAsync`
36 (blind overwrite). On a failed load, do **not** overwrite with defaults — abort
37 the save so you don't wipe good data.
386. **Use `OrderedDataStore` for ranked data** (leaderboards) via `GetSortedAsync`.
39 Test by joining, changing data, rejoining, and confirming it persisted.
40
41## Patterns
42
43### 1. Load on join (pcall-guarded)
44
45```lua
46local DataStoreService = game:GetService("DataStoreService")
47local Players = game:GetService("Players")
48local store = DataStoreService:GetDataStore("PlayerData")
49
50local DEFAULT = { Coins = 0, Level = 1 }
51
52Players.PlayerAdded:Connect(function(player)
53 local key = "Player_" .. player.UserId
54 local ok, data = pcall(function()
55 return store:GetAsync(key)
56 end)
57
58 if not ok then
59 -- Load FAILED (network). Do not treat as a new player; flag so we never save
60 -- over their real data with defaults.
61 warn("Load failed for", player.Name, data)
62 player:SetAttribute("DataLoaded", false)
63 return
64 end
65
66 player:SetAttribute("DataLoaded", true)
67 local profile = data or DEFAULT -- nil == genuinely new player
68 applyToLeaderstats(player, profile)
69end)
70```
71
72### 2. Save with UpdateAsync (multi-server safe)
73
74```lua
75-- UpdateAsync reads the latest value, then writes what the callback returns.
76-- The callback MUST NOT yield (no task.wait, no further Async calls inside it).
77local function savePlayer(player)
78 if player:GetAttribute("DataLoaded") == false then return end -- never overwrite on a bad load
79 local key = "Player_" .. player.UserId
80 local newData = gatherDataFor(player) -- a plain table of serializable values
81
82 local ok, err = pcall(function()
83 store:UpdateAsync(key, function(old)
84 -- merge/decide here; return nil to cancel the write
85 return newData
86 end)
87 end)
88 if not ok then warn("Save failed for", player.Name, err) end
89end
90```
91
92### 3. Save on leave AND on shutdown
93
94```lua
95Players.PlayerRemoving:Connect(savePlayer)
96
97-- BindToClose runs when the server shuts down; save everyone still in.
98-- It has a limited time budget, so save in parallel and yield until done.
99game:BindToClose(function()
100 local players = Players:GetPlayers()
101 local remaining = #players
102 if remaining == 0 then return end
103 for _, player in players do
104 task.spawn(function()
105 savePlayer(player)
106 remaining -= 1
107 end)
108 end
109 while remaining > 0 do task.wait() end
110end)
111```
112
113### 4. Retry with backoff (transient failures)
114
115```lua
116local function withRetry(fn, attempts)
117 attempts = attempts or 3
118 for i = 1, attempts do
119 local ok, result = pcall(fn)
120 if ok then return true, result end
121 if i < attempts then task.wait(2 ^ i) end -- 2s, 4s, ... backoff
122 end
123 return false
124end
125
126local ok, data = withRetry(function() return store:GetAsync(key) end)
127```
128
129### 5. Increment a counter
130
131```lua
132-- IncrementAsync is a convenience for integer read-modify-write (still wrap it).
133local ok, newTotal = pcall(function()
134 return store:IncrementAsync("Visits_" .. player.UserId, 1)
135end)
136```
137
138### 6. Leaderboard with OrderedDataStore
139
140```lua
141local boards = DataStoreService:GetOrderedDataStore("Coins")
142
143-- Write a player's score (call when it changes, not every frame).
144pcall(function() boards:SetAsync("Player_" .. player.UserId, coins) end)
145
146-- Read the top 10, descending.
147local ok, pages = pcall(function()
148 return boards:GetSortedAsync(false, 10) -- ascending=false → highest first
149end)
150if ok then
151 for rank, entry in ipairs(pages:GetCurrentPage()) do
152 print(rank, entry.key, entry.value) -- entry.value is the number
153 end
154end
155```
156
157## Pitfalls
158
159- **Unhandled failure wipes progress** → always `pcall` Async calls; on a failed
160 *load*, mark the session and refuse to *save* so defaults never overwrite real data.
161- **`SetAsync` race between servers** → two servers writing the same key can clobber
162 each other. Use `UpdateAsync` for read-modify-write so each write sees the latest.
163- **Yielding inside the `UpdateAsync` callback** → the callback can't call
164 `task.wait` or other Async functions; compute the new value beforehand and return it.
165- **No `BindToClose` save** → players in the server at shutdown lose unsaved progress;
166 add `game:BindToClose` and wait for saves to finish within its budget.
167- **Throttling / "too many requests"** → respect per-key and per-minute limits; don't
168 save on every value change. Batch and save on a timer / on leave. `GetAsync` is
169 cached briefly, so immediate re-reads may be stale.
170- **Storing non-serializable values** → only JSON-serializable data persists: numbers,
171 strings, booleans, and tables with string/number keys. `Instance`s, `Vector3`,
172 `CFrame`, and functions do not — serialize them to plain tables first.
173- **Testing without API access** → DataStores silently can't be used in Studio until
174 *Enable Studio Access to API Services* is on (and they don't work from a
175 `LocalScript`).
176- **`DataStoreKeyInfo` is nil for ordered stores** → `OrderedDataStore` doesn't
177 support versioning/metadata; use a regular `DataStore` when you need those.
178
179## References
180
181- For session locking (preventing duplicate data across servers), versioning/
182 metadata with `DataStoreSetOptions`, ordered-store pagination
183 (`AdvanceToNextPageAsync`), the key error codes and request limits, and
184 Right-to-be-Forgotten compliance, read `references/sessions-and-limits.md`.
185
186## Related skills
187
188- `roblox-luau` — services, instances, events, and the server/client model.
189- `save-systems` — engine-agnostic serialization, slots, and migration.