DataStore is Roblox's official system for saving player data across game sessions. Without it, progress vanishes when a player leaves. With it, you can store coins, levels, inventory, and any other progress permanently.
Saving data correctly means handling errors, respecting rate limits, and making sure data actually persists. Let's build a complete system from the ground up.
What Is DataStore?
DataStore is a server-side tool that stores key-value pairs in Roblox's cloud. Each key holds one value (a string, number, table, or JSON). Access is restricted to server scripts only—clients cannot read or write to DataStore.
There are two main types:
- Standard DataStore — General-purpose storage. One per game.
- Ordered DataStore — Sorted by value. Useful for leaderboards.
In this guide, we'll focus on standard DataStore for player progress.
Setting Up DataStore
First, enable API access in Studio settings:
- Go to File → Game Settings.
- Click the Security tab.
- Toggle Enable Studio Access to API Services to ON.
This lets you test DataStore in Studio. On a live server, it works without extra setup.
Basic DataStore Structure
Here's a complete script for saving and loading player data. Place this in ServerScriptService:
local DataStoreService = game:GetService("DataStoreService")
local playerDataStore = DataStoreService:GetDataStore("PlayerData")
local Players = game:GetService("Players")
-- When a player joins, load their data
Players.PlayerAdded:Connect(function(player)
local userId = player.UserId
local success, data = pcall(function()
return playerDataStore:GetAsync(userId)
end)
if success then
if data then
player:SetAttribute("Level", data.level)
player:SetAttribute("Coins", data.coins)
print(player.Name .. " loaded: Level " .. data.level .. ", Coins " .. data.coins)
else
-- New player: set defaults
player:SetAttribute("Level", 1)
player:SetAttribute("Coins", 0)
print(player.Name .. " is new, defaults set")
end
else
warn("Failed to load data for " .. player.Name)
end
end)
-- When a player leaves, save their data
Players.PlayerRemoving:Connect(function(player)
local userId = player.UserId
local level = player:GetAttribute("Level") or 1
local coins = player:GetAttribute("Coins") or 0
local success, err = pcall(function()
playerDataStore:SetAsync(userId, {
level = level,
coins = coins
})
end)
if success then
print(player.Name .. " data saved")
else
warn("Failed to save " .. player.Name .. ": " .. err)
end
end)
How it works:
GetDataStore("PlayerData")creates or accesses a DataStore named "PlayerData".GetAsync(userId)retrieves a player's stored data (or nil if new).SetAsync(userId, data)saves a table to that player's key.pcallwraps the calls in error handling so network failures don't crash the game.
The script uses player attributes to store data in memory. When the player leaves, it saves to DataStore.
Adding Error Recovery
Network failures happen. If SetAsync fails, you should retry before the server stops:
local function savePlayerData(player)
local userId = player.UserId
local level = player:GetAttribute("Level") or 1
local coins = player:GetAttribute("Coins") or 0
local maxRetries = 3
local retryDelay = 2
for attempt = 1, maxRetries do
local success, err = pcall(function()
playerDataStore:SetAsync(userId, {
level = level,
coins = coins
})
end)
if success then
print(player.Name .. " data saved on attempt " .. attempt)
return true
else
warn("Save attempt " .. attempt .. " failed: " .. err)
if attempt < maxRetries then
wait(retryDelay)
end
end
end
warn("Failed to save " .. player.Name .. " after " .. maxRetries .. " attempts")
return false
end
-- Use in PlayerRemoving
Players.PlayerRemoving:Connect(function(player)
savePlayerData(player)
end)
This tries up to 3 times with a 2-second wait between attempts. If all retries fail, the script logs a warning but doesn't crash the game.
Respecting Rate Limits
DataStore has built-in rate limits to prevent abuse. You get a certain number of reads and writes per minute per DataStore. Exceed the limit, and requests fail.
Best practices:
- Save on leave, not every change. Don't call SetAsync every time a player earns 1 coin.
- Batch updates. Combine multiple changes into one save.
- Use auto-save sparingly. If you auto-save every 60 seconds, do it for essential data only.
Here's an auto-save system that saves every 5 minutes:
local autoSaveInterval = 300 -- 5 minutes in seconds
Players.PlayerAdded:Connect(function(player)
-- Load data (see earlier script)
-- Start auto-save loop
local connection
connection = game:GetService("RunService").Heartbeat:Connect(function()
if not player.Parent then
connection:Disconnect()
return
end
end)
-- Auto-save every 5 minutes
task.spawn(function()
while player.Parent do
wait(autoSaveInterval)
if player.Parent then
local userId = player.UserId
local level = player:GetAttribute("Level") or 1
local coins = player:GetAttribute("Coins") or 0
local success = pcall(function()
playerDataStore:SetAsync(userId, {
level = level,
coins = coins
})
end)
if success then
print(player.Name .. " auto-saved")
else
warn("Auto-save failed for " .. player.Name)
end
end
end
end)
end)
This saves player data every 300 seconds (5 minutes). Adjust the interval based on your game's needs.
task.spawn to run auto-save in the background without blocking the main loop. This prevents lag.Handling Data Corruption
Sometimes saved data is incomplete or corrupted. Always validate data when loading:
local function loadPlayerData(player)
local userId = player.UserId
local success, data = pcall(function()
return playerDataStore:GetAsync(userId)
end)
if success and data then
-- Validate each field exists and is the right type
local level = tonumber(data.level)
local coins = tonumber(data.coins)
if level and coins and level >= 1 and coins >= 0 then
player:SetAttribute("Level", level)
player:SetAttribute("Coins", coins)
print(player.Name .. " data loaded and validated")
else
warn(player.Name .. " data invalid, using defaults")
player:SetAttribute("Level", 1)
player:SetAttribute("Coins", 0)
end
elseif not success then
warn("Failed to load " .. player.Name .. ": " .. data)
player:SetAttribute("Level", 1)
player:SetAttribute("Coins", 0)
else
-- New player
player:SetAttribute("Level", 1)
player:SetAttribute("Coins", 0)
end
end
Players.PlayerAdded:Connect(function(player)
loadPlayerData(player)
end)
This script checks that level and coins are numbers, and that they're within reasonable ranges. If anything is wrong, it resets to defaults instead of applying bad data.
Testing DataStore in Studio
To test DataStore locally:
- Enable Studio Access to API Services (see earlier section).
- Run your game in Studio with Run or Run as Client.
- Open the Command Bar at the bottom of Studio.
- Use
task.wait()to pause, then type commands to check data.
To manually check saved data:
local ds = game:GetService("DataStoreService"):GetDataStore("PlayerData")
print(ds:GetAsync(1234567890)) -- Replace with a real UserId
This prints the saved data for that player. If it returns nil, either the player has no saves or the key is wrong.
DataStore Best Practices
- Only access DataStore on the server. Never in LocalScripts. Use RemoteEvents to ask the server to fetch or save data.
- Cache data in player attributes. Read attributes during gameplay, not DataStore. This avoids hitting rate limits.
- Save before deletion. Always save data in PlayerRemoving, even if auto-save is on. Network delays might prevent auto-saves from finishing.
- Use descriptive names. Name DataStores clearly: "PlayerStats", "InventoryData", "QuestProgress". This helps when debugging.
- Version your data. Add a version field to your save format. If you change the structure later, you can migrate old saves.
- Test backups. Roblox keeps automatic backups. If data is deleted by accident, contact Roblox Support to restore from a backup.
Common Mistakes
Forgetting pcall: Calling GetAsync or SetAsync without pcall causes the script to crash on network errors. Always wrap DataStore calls.
Saving on every change: Saving coins every time a player earns 1 coin burns through your rate limit fast. Save only on leave or at long intervals.
Not validating data: If a player manually edits their save (in tests), you might load nonsense values. Always check that loaded data makes sense before using it.
Assuming data exists: New players have no save. Always provide default values if GetAsync returns nil.