| name | roblox-datastores |
| description | Use when implementing player data persistence in Roblox, saving/loading player stats or inventory, building leaderboards with ordered datastores, handling data migration between versions, diagnosing data loss issues, or adding auto-save and shutdown-safe data handling with DataStoreService. |
roblox-datastores
Reference for Roblox DataStoreService — saving, loading, and managing player data on the server.
Quick Reference
| Method | Signature | Notes |
|---|
GetDataStore | DSS:GetDataStore(name, scope?) | Returns a GlobalDataStore |
GetOrderedDataStore | DSS:GetOrderedDataStore(name, scope?) | For leaderboards |
GetAsync | store:GetAsync(key) | Returns value or nil |
SetAsync | store:SetAsync(key, value) | No return value needed |
UpdateAsync | store:UpdateAsync(key, fn) | Atomic read-modify-write |
RemoveAsync | store:RemoveAsync(key) | Deletes key, returns old value |
GetSortedAsync | orderedStore:GetSortedAsync(asc, pageSize) | Returns DataStorePages |
Basic Setup
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local playerStore = DataStoreService:GetDataStore("PlayerData_v1")
local DEFAULT_DATA = {
coins = 0,
level = 1,
xp = 0,
}
Loading Data (GetAsync + pcall)
Always wrap datastore calls in pcall. They can fail due to network issues or rate limits.
local function loadData(player)
local key = "player_" .. player.UserId
local success, data = pcall(function()
return playerStore:GetAsync(key)
end)
if success then
local result = {}
for k, v in pairs(DEFAULT_DATA) do result[k] = v end
if data then
for k, v in pairs(data) do result[k] = v end
end
return result
else
warn("Failed to load data for", player.Name, ":", data)
return nil
end
end
Saving Data (SetAsync vs UpdateAsync)
Use SetAsync for simple overwrites. Use UpdateAsync when the value must be based on the current stored value (e.g., incrementing a counter safely across servers).
local function saveData(player, data)
local key = "player_" .. player.UserId
local success, err = pcall(function()
playerStore:SetAsync(key, data)
end)
if not success then
warn("Failed to save data for", player.Name, ":", err)
end
end
local function addCoinsAtomic(userId, amount)
local key = "player_" .. userId
pcall(function()
playerStore:UpdateAsync(key, function(current)
current = current or { coins = 0 }
current.coins = current.coins + amount
return current
end)
end)
end
Retry Logic
local MAX_RETRIES = 3
local RETRY_DELAY = 2
local function safeGet(store, key)
for attempt = 1, MAX_RETRIES do
local success, result = pcall(function()
return store:GetAsync(key)
end)
if success then return true, result end
warn(string.format("GetAsync attempt %d/%d failed: %s", attempt, MAX_RETRIES, result))
if attempt < MAX_RETRIES then task.wait(RETRY_DELAY) end
end
return false, nil
end
Auto-Save: PlayerRemoving + BindToClose
Server shutdown without BindToClose silently discards unsaved data.
local sessionData = {}
Players.PlayerAdded:Connect(function(player)
local data = loadData(player)
if data then
sessionData[player.UserId] = data
else
player:Kick("Could not load your data. Please rejoin.")
end
end)
Players.PlayerRemoving:Connect(function(player)
local data = sessionData[player.UserId]
if data then
saveData(player, data)
sessionData[player.UserId] = nil
end
end)
game:BindToClose(function()
for userId, data in pairs(sessionData) do
local key = "player_" .. userId
pcall(function()
playerStore:SetAsync(key, data)
end)
end
end)
Ordered DataStores (Leaderboards)
Values must be positive integers.
local coinsLeaderboard = DataStoreService:GetOrderedDataStore("Coins_v1")
local function setLeaderboardScore(userId, coins)
pcall(function()
coinsLeaderboard:SetAsync("player_" .. userId, math.floor(coins))
end)
end
local function getTopPlayers(count)
local success, pages = pcall(function()
return coinsLeaderboard:GetSortedAsync(false, count)
end)
if not success then return {} end
local results = {}
for rank, entry in ipairs(pages:GetCurrentPage()) do
table.insert(results, { rank = rank, userId = entry.key, score = entry.value })
end
return results
end
Data Versioning / Migration
Include a _version field and migrate in the load path.
local CURRENT_VERSION = 2
local function migrateData(data)
local version = data._version or 1
if version < 2 then
data.coins = data.gold or 0
data.gold = nil
data._version = 2
end
return data
end
Use a versioned datastore name (PlayerData_v2) for breaking schema changes.
Common Mistakes
| Mistake | Consequence | Fix |
|---|
No pcall around datastore calls | Unhandled error crashes the script | Always wrap in pcall |
Saving on every Changed event | Hits rate limits (60 + numPlayers×10 writes/min) | Throttle; save on remove + periodic interval |
No BindToClose handler | Data lost on server shutdown | Always flush all sessions in BindToClose |
| Giving default data on load failure | Player silently loses progress | Return nil on failure; kick or retry |
SetAsync for atomic counters | Race condition across servers | Use UpdateAsync for read-modify-write |
| Storing Instances or functions | Data silently drops | Store only strings, numbers, booleans, plain tables |
| Reusing datastore name after schema change | Old shape clashes with new code | Append _v2, _v3 to name on breaking changes |