DataStore Explained: How Saving Player Data Works in Roblox Games
DataStore is the invisible system behind every saved coin, level and inventory in Roblox games. Here is how it works, a complete save/load example for leaderstats, and the mistakes that wipe player data.
In this guide
What DataStore Actually Is When Saving Must Happen GetAsync, SetAsync and UpdateAsync in Plain Language A Complete leaderstats Save/Load Example Why Players Actually Lose Data Testing in Studio: The Setting Everyone Forgets Limits and Throttling, Without the Mathematics Headache Frequently Asked Questions Why does my data save in the real game but not when I test in Studio? Do I need DataStore for a game with no saving, like a simple obby with no coins? What happens to saved data if I rename my DataStore, for example from "Coins_v1" to "Coins_v2"?Almost every question in a Roblox developer forum eventually becomes the same question: "Why did my player's coins reset?" The system behind saving is called DataStore, and it is not difficult once somebody explains what it actually is, instead of just handing you a script to copy. This guide explains DataStore in plain language, shows a complete working save and load example for the classic leaderstats setup, and covers the boring details, like request limits and a Studio setting almost everyone forgets, that decide whether your players keep their progress or lose it.
What DataStore Actually Is
A DataStore is a storage box that lives on Roblox's servers, not inside your place file and not on the player's computer. Your game script asks Roblox to store a piece of data under a key (a unique name), and later asks for it back using the same key. For player data, the key is almost always built from the player's UserId, for example "Player_4829103", because a UserId never changes, while a username can.
Two rules follow from this design. First, data belongs to the experience, not to one server: a player can join any server of your game, anywhere in the world, and load the same saved coins. Second, your script is responsible for everything. Roblox will not save anything automatically. If your script never asks, nothing is stored, and when the player leaves, whatever was only in the server's memory disappears forever. Almost every "data loss" bug is some version of the script not asking at the right moment, or asking and failing without anyone noticing.
When Saving Must Happen
You cannot save "at the end" in the way beginners imagine, because there is no neat end screen for a Roblox session. Saving happens in response to events:
- PlayerRemoving fires when a player leaves the game. This is the main save moment. When it fires, your script should immediately collect that player's data and store it.
- BindToClose is for the case where the whole server shuts down, for example when you close all servers for an update or a server is terminated. BindToClose gives your function a short window (up to about 30 seconds) to finish before the server dies. Without it, everyone online during a shutdown can lose everything since their last save.
- Autosave on a timer is the safety net most real games add: save everyone every few minutes (commonly every 3–5 minutes, staggered so all players are not saved in the same second). If a server crashes without warning, players then lose minutes of progress instead of hours.
GetAsync, SetAsync and UpdateAsync in Plain Language
| Function | What it does | Use it for |
|---|---|---|
GetAsync(key) | Reads the saved value for a key and gives it back to your script. Returns nothing (nil) if the player has never saved. | Loading a player's data when they join. |
SetAsync(key, value) | Writes a value, completely replacing whatever was there before, whether your script has seen the old value or not. | First-time saves, or data where only one server could ever be writing. |
UpdateAsync(key, function) | Reads the current saved value, hands it to your function, and stores whatever your function returns, as one operation. | Adding coins or items safely when an old copy of the data might exist, because the change is calculated from the newest saved value, not a stale one. |
The SetAsync versus UpdateAsync difference matters in one specific trap: the same player somehow being active in two places at once, or an old server writing after a newer one. SetAsync blindly overwrites, so the stale server's older data wins and recent progress vanishes. UpdateAsync at least builds on the value that is actually stored. For simple single-place games, a careful SetAsync pattern works; the moment your game has multiple places or trading, UpdateAsync and stricter design become important.
A Complete leaderstats Save/Load Example
This is the smallest honest version of the pattern used by thousands of games. It creates a Coins leaderstat, loads the saved amount when a player joins, and saves when they leave and when the server closes. Put it in a normal Script in ServerScriptService, not a LocalScript; DataStore can only be reached from the server.
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local coinsStore = DataStoreService:GetDataStore("Coins_v1")
local function onPlayerAdded(player)
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
local coins = Instance.new("IntValue")
coins.Name = "Coins"
coins.Value = 0
coins.Parent = leaderstats
leaderstats.Parent = player
local success, saved = pcall(function()
return coinsStore:GetAsync("Player_" .. player.UserId)
end)
if success and saved ~= nil then
coins.Value = saved
end
end
local function savePlayer(player)
local coins = player:FindFirstChild("leaderstats") and player.leaderstats:FindFirstChild("Coins")
if not coins then return end
local success, err = pcall(function()
coinsStore:SetAsync("Player_" .. player.UserId, coins.Value)
end)
if not success then
warn("Save failed for " .. player.Name .. ": " .. tostring(err))
end
end
Players.PlayerAdded:Connect(onPlayerAdded)
Players.PlayerRemoving:Connect(savePlayer)
game:BindToClose(function()
for _, player in ipairs(Players:GetPlayers()) do
savePlayer(player)
end
end)
Three details in this script are doing more work than they look like. The pcall wrappers mean a failed DataStore request shows a warning instead of breaking the whole script; an unprotected failed load can leave a player with a fresh 0 that later gets saved over their real data, which is the worst bug in this whole topic. The store name "Coins_v1" has a version baked into it, because changing the name is the clean way to start fresh data after you change your data format. And for anything beyond one number, real games save a table (coins, level, inventory together) under one key, because tables count as one request while separate stores for every stat burn through the limits described below.
Why Players Actually Lose Data
- Saving a failed load. The load request errors, the script continues with default values, and the leave-save overwrites the real data with defaults. Always check that the load succeeded before allowing a save.
- No BindToClose. The developer updates the game, all servers shut down, and nobody's leave event gets a chance to finish, or it fires while requests are already failing.
- Editing live data from Studio. With Studio API access enabled (next section), pressing Play in Studio can load and save against the real live data. One careless test with a test account is harmless; testing while impersonating real values is not.
- Hitting request limits. If saves are being rejected for throttling, they simply do not happen, and unless you log failures, you find out from angry players.
- Two sessions writing the same key. A player rapidly rejoining, or being in two connected places at once, can produce an older write landing after a newer one. This is often called the session-locking problem: standard DataStores do not lock a key to one session for you, so your design (single-writer rules, UpdateAsync, checking timestamps inside the saved table) has to prevent stale overwrites.
Testing in Studio: The Setting Everyone Forgets
By default, Studio cannot reach your game's live DataStores, and your load test will silently return nothing. The fix is Game Settings > Security > Enable Studio Access to API Services, switched on, then publish the game once. Two warnings. First, after enabling it, Studio reads and writes real player data for your published game, so test with your own account. Second, Studio has a habit of making beginners think saving is broken when actually the game was never published after flipping the setting, publishing is what applies it.
Limits and Throttling, Without the Mathematics Headache
Roblox limits how many DataStore requests an experience can make, because the service is shared. The practical facts you need are these. Each type of request (get, set, update) has its own budget that grows with the number of players in the server, roughly on the scale of 60 requests per minute plus 10 more per player for a server. A single key also has a cooldown: the same key cannot be written more often than about once every 6 seconds, and a saved value can be up to about 4 MB of text, which is far more than a normal player profile needs. When you exceed a budget, requests start failing with throttling errors, which is why the pattern in this guide saves on join, on leave, and on a spaced-out timer, rather than every time a coin changes. Saving on every change is the number-one design mistake in beginner games: it wastes the budget, risks throttling at the busiest moments, and saves nothing extra that a leave-save would not have covered.
Keep those rules, wrap every request in pcall, never save data you failed to load, and your players will keep their coins. That is genuinely most of DataStore mastery; everything else is optimisation.
Frequently Asked Questions
Why does my data save in the real game but not when I test in Studio?
Studio cannot use DataStores until you turn on "Enable Studio Access to API Services" in Game Settings > Security and publish the game. Also remember that testing in Studio then touches your live data, so test on your own account, not with values copied from a real player.
Do I need DataStore for a game with no saving, like a simple obby with no coins?
No. If nothing needs to survive after a player leaves, you do not need DataStore at all. The moment you add anything players expect to keep, wins, coins, unlocked levels, items, you need it, and it is far easier to add at the start than to retrofit after players already have progress stored in their heads but nowhere else.
What happens to saved data if I rename my DataStore, for example from "Coins_v1" to "Coins_v2"?
The new name is a completely separate, empty storage box. Every player starts from nothing under the new key, and the old data still exists under the old name but is never read again. Developers use this deliberately to reset data after a big format change, and beginners do it accidentally by "tidying up" a name, so treat store names as permanent once your game has real players.