Error reference
Roblox DataStore not saving
Losing player data is the one bug that costs you the player permanently. It is also the bug most likely to look fine in testing: you save, you rejoin, it works — and then it fails on a live server for reasons that never occur while you are the only person in the place.
Rule before anything else: every DataStore call is a network request that is allowed to fail. If it is not inside pcall, a failure throws and takes the rest of your save with it — usually without you noticing.
1. No pcall, so failures are invisible
-- a failed request kills the whole thread here
store:SetAsync(key, data)
-- a failed request is something you can see and retry
local ok, err = pcall(function()
store:SetAsync(key, data)
end)
if not ok then
warn(("save failed for %s: %s"):format(key, tostring(err)))
end
2. Saving only on PlayerRemoving
This is the big one, and it is why data saves in Studio but disappears in production. PlayerRemoving fires when someone leaves — but when the server shuts down, the place gets a short window to finish work and then dies. Without BindToClose, everyone still in that server loses whatever changed since their last save.
local savingPlayers = 0
local function savePlayer(player)
savingPlayers += 1
local ok, err = pcall(function()
store:UpdateAsync(tostring(player.UserId), function(old)
return mergeProfile(old, Profiles[player])
end)
end)
if not ok then warn("save failed:", err) end
savingPlayers -= 1
end
Players.PlayerRemoving:Connect(savePlayer)
game:BindToClose(function()
for _, player in Players:GetPlayers() do
task.spawn(savePlayer, player)
end
-- hold the shutdown until the in-flight saves finish
while savingPlayers > 0 do
task.wait()
end
end)
The shutdown window is short — treat it as a handful of seconds, not a place to do slow work.
3. API services are off, or the place is unpublished
In Studio, DataStores need Game Settings → Security → Enable Studio Access to API Services. And an unpublished place has no universe to store anything against. If every call fails immediately in Studio and works in the live game, this is almost always why.
4. SetAsync where you needed UpdateAsync
SetAsync overwrites whatever is stored. If a value depends on what is already there — coins, inventory, a counter — two writes can race and one silently wins. UpdateAsync hands your transform function the newest stored value and writes atomically.
-- race: reads, then writes something already stale
local coins = store:GetAsync(key)
store:SetAsync(key, coins + 100)
-- atomic: the transform sees the current value
store:UpdateAsync(key, function(old)
return (old or 0) + 100
end)
5. You are over the request budget
“Request was added to queue” means you are being throttled. Saving on every coin pickup will do it. Save on a timer plus on leave, coalesce changes in memory between saves, and check the budget before a burst:
local budget = DataStoreService:GetRequestBudgetForRequestType(
Enum.DataStoreRequestType.UpdateAsync
)
if budget < 1 then
task.wait(6)
end
6. The value cannot be serialised
DataStores store JSON-shaped data. Instances, functions, Vector3, Color3 and CFrame are not valid — convert them to plain numbers first. Tables must use either all string keys or a clean array; a table with mixed keys, or holes in the middle of an array, does not survive the round trip intact.
7. No session locking, so a rejoin overwrites a live session
A player leaving one server and joining another before the first save completes can end up with the old server writing after the new one loaded. Anything that reads and writes the same key from two places needs a session marker — store a job id and timestamp with the profile, refuse to load a key another session still holds, and let the lock expire on a timeout so a crashed server does not lock someone out permanently.
How to actually verify a save
- Change something, then rejoin. Printing “saved” proves the code ran, not that the write landed.
- Test with the server shutting down under you, not just with one player leaving.
- Test two clients changing the same profile, and a rejoin immediately after leaving.
- Watch Output for throttling warnings while doing it — a save that queues is a save that may not land before shutdown.
- Read the value back through a separate path and compare it to what you expected, rather than trusting the write's return.
Why AI-generated save code fails this way: the happy path is easy to write and looks complete. pcall, BindToClose, session locking and throttling are all things you only discover you needed after real players lose progress — none of them are visible in a code review of a script that “works”.
Test the shutdown path before your players do
Baseplate replays deterministic Studio scenarios with evidence, and its Safe DataOps lab snapshots a live key, previews a migration and supports a confirmed rollback before anything touches production data.
Download Baseplate free