How to stop a failed data store load wiping player data
Treating a failed load as a new player hands out default data, which is then saved over the real data. How to keep failed loads apart from new players.
What it looks like
The server loads a player's data, and when the call fails it carries on as if the player were new: it hands out default data and later saves it over their real data.
Luau
local ok, data = pcall(store.GetAsync, store, key) profiles[player] = if ok and data then data else defaultProfile()
How to detect it
- Find every
GetAsync,UpdateAsyncor library call that loads saved data when a player joins. - Check what happens when it fails. Data store calls can error during outages and throttling, which is why they must be wrapped in
pcall. An unwrapped call that errors stops the join script, which is a finding on its own. - A failed call and "no data yet" are different results.
ok == falsemeans the load failed.ok == truewithnilmeans the player is new. Code that treats both the same gives a player who failed to load a fresh profile. - Then follow the save path. If a profile that never loaded can still be saved, on leave, on an autosave or at shutdown, the defaults overwrite the real data.
What an exploiter could do
No exploit is needed. During a data store outage or heavy throttling, every player who joins gets fresh data, and when they leave that fresh data replaces everything they had. Players lose their progress and purchases, and the save can't be undone from the game.
How to fix it
- Keep "failed" apart from "new". Retry a failed load a few times with a short wait between attempts.
- If it still fails, don't let that profile save. Mark it as not loaded, and either kick the player with a message asking them to rejoin or let them play without saving and tell them so.
- Save only profiles that loaded successfully.
Vulnerable example
Vulnerable
local DataStoreService = game:GetService("DataStoreService") local Players = game:GetService("Players") local store = DataStoreService:GetDataStore("Coins") local coins: { [Player]: number } = {} Players.PlayerAdded:Connect(function(player) local ok, saved = pcall(function() return store:GetAsync(tostring(player.UserId)) end) coins[player] = if ok and typeof(saved) == "number" then saved else 0 end) Players.PlayerRemoving:Connect(function(player) local amount = coins[player] coins[player] = nil if amount then pcall(function() store:SetAsync(tostring(player.UserId), amount) end) end end)
Fixed example
Fixed
local DataStoreService = game:GetService("DataStoreService") local Players = game:GetService("Players") local store = DataStoreService:GetDataStore("Coins") local coins: { [Player]: number } = {} local function load(player: Player): (boolean, unknown) for attempt = 1, 3 do local ok, saved = pcall(function() return store:GetAsync(tostring(player.UserId)) end) if ok then return true, saved end task.wait(attempt * 2) end return false, nil end Players.PlayerAdded:Connect(function(player) local ok, saved = load(player) if player.Parent ~= Players then return end if not ok then player:Kick("We couldn't load your data. Please rejoin in a moment.") return end coins[player] = if typeof(saved) == "number" then saved else 0 end) Players.PlayerRemoving:Connect(function(player) local amount = coins[player] coins[player] = nil if amount then pcall(function() store:SetAsync(tostring(player.UserId), amount) end) end end)
Common false positives
- A well-known profile library handles failed loads itself. Check how the game calls it and what it does when a load fails, but don't report the library's internals.
- Defaults used only while a load is in progress, with saving blocked until it finishes, are fine.
- A save that only merges into the stored value inside
UpdateAsync, such as adding to a set of unlocked items, can't overwrite real data after a failed load. Mention that the player may not see their saved data that session, rather than reporting it. - A load that fails and leaves nothing saveable, for example because the profile is never created, is safe even without a retry. Mention that a retry or a message would be kinder.