A Roblox DataStore save script that never wipes player data
Short answer
Load with GetAsync inside pcall and retry; if the load fails, kick the player instead of handing out a blank profile. Save with UpdateAsync on leave, every few minutes, and for every player in game:BindToClose. Never save a profile you failed to load: that one rule prevents most lost-save reports.
- In Studio, open Game Settings > Security and turn on Enable Studio Access to API Services (needed to test DataStores in Studio).
- Put the script in ServerScriptService. Change DEFAULT to your own fields.
- Read and change profiles[player] from your other server scripts instead of calling the DataStore directly.
- Test: join, change a value, leave, rejoin. Then stop the server with players in it to test BindToClose.
Copy-paste code
ServerScriptService/PlayerData.server.lua
local Players = game:GetService("Players")
local DataStoreService = game:GetService("DataStoreService")
local store = DataStoreService:GetDataStore("PlayerData_v1")
local DEFAULT = { coins = 0, level = 1 }
local profiles = {} -- [player] = data, only when the load worked
local function key(player) return "u_" .. player.UserId end
local function load(player)
for attempt = 1, 3 do
local ok, data = pcall(store.GetAsync, store, key(player))
if ok then
profiles[player] = data or table.clone(DEFAULT)
return
end
task.wait(attempt * 2)
end
player:Kick("Your data could not load. Please rejoin.")
end
local function save(player)
local data = profiles[player]
if not data then return end -- never write a profile that failed to load
pcall(store.UpdateAsync, store, key(player), function()
return data
end)
end
Players.PlayerAdded:Connect(load)
Players.PlayerRemoving:Connect(function(player)
save(player)
profiles[player] = nil
end)
game:BindToClose(function()
for _, player in Players:GetPlayers() do
task.spawn(save, player)
end
task.wait(3)
end)
task.spawn(function() -- autosave every 3 minutes
while true do
task.wait(180)
for _, player in Players:GetPlayers() do save(player) end
end
end)Good to know
Why UpdateAsync instead of SetAsync: UpdateAsync gives you the stored value first, so you can refuse to overwrite newer data (for example, a version number that went backwards). SetAsync writes blindly.
What this script does not do: stop two servers saving the same player at once (a player who rejoins fast can be loaded on server B before server A finished saving). That needs session locking, which is what ProfileService-style modules add.
Studwright: Roblox systems that never lose saves
from $12Studwright's SaveData adds the session lock this script lacks (one server owns a save at a time), and the kit has 15 more tested modules: Receipts, Codes, DailyRewards, Leaderboard, Inventory, Currency and others.
Secure checkout · 14-day fix-or-refund if it doesn't work as described (terms)Questions
Why did my player's data reset?
Almost always a failed or slow GetAsync that the script treated as a new player, followed by a save of the blank profile. The guard in save() stops that write.
How often can I save?
Roblox allows 60 + numPlayers × 40 writes per minute per server. A save on leave plus an autosave every 3 minutes uses a tiny part of it.
Do I need DataStore2 or ProfileService?
Not for a small game. You need session locking once players can hop servers quickly, or once losing an item costs real Robux.
Free: the Roblox Starter Kit
7 free Luau modules (damage, pets, shop formatting, admin audit log, tycoon guards and more) plus 3 free calculators, then 5 short emails with the scripting fixes we see go wrong most often.