Every good game needs to remember what players did. DataStore is Roblox's way of saving that information to the servers so it stays safe. Whether you're tracking level, currency, inventory, or stats — DataStore keeps it persistent.

Let's build a working system from scratch. You'll see exactly where each script goes and why it works.

What Is DataStore?

DataStore is a built-in Roblox service that stores player data on Roblox's servers. When a player leaves your game and comes back a week later, their data is still there. Unlike variables in your script (which disappear when the server restarts), DataStore data is permanent.

There are two main types:

  • Regular DataStore — stores one value per key (best for simple data like level or coins).
  • OrderedDataStore — keeps data sorted by value (best for leaderboards).

For now, we'll focus on regular DataStore since it's what most games need.

Setting Up DataStores

First, enable DataStore in your game settings. Go to HomeGame SettingsSecurity, and make sure Allow HTTP Requests is checked. This lets your scripts communicate with Roblox's servers.

Next, get access to the DataStore service at the top of your script:

Get DataStore Service
local DataStoreService = game:GetService("DataStoreService")
local playerDataStore = DataStoreService:GetDataStore("PlayerData")

The string "PlayerData" is the name of your DataStore. You can have multiple DataStores for different purposes — one for levels, one for inventory, etc. Roblox will create it automatically the first time you access it.

Saving Player Data

The safest place to save data is when a player leaves the game. Use the PlayerRemoving event in a Script inside ServerScriptService.

Basic Save Script (ServerScriptService)
local DataStoreService = game:GetService("DataStoreService")
local playerDataStore = DataStoreService:GetDataStore("PlayerData")

local Players = game:GetService("Players")

Players.PlayerRemoving:Connect(function(player)
    local key = "Player_" .. player.UserId
    local data = {
        level = player:FindFirstChild("Level").Value,
        coins = player:FindFirstChild("Coins").Value
    }
    
    local success, errorMessage = pcall(function()
        playerDataStore:SetAsync(key, data)
    end)
    
    if success then
        print("Saved data for " .. player.Name)
    else
        print("Failed to save data: " .. errorMessage)
    end
end)

Let's break this down:

  • key = "Player_" .. player.UserId — Each player gets a unique key based on their user ID. This makes sure one player's data doesn't overwrite another's.
  • data = {...} — A table holding all the player's stats. You can add as many fields as you need.
  • pcall(...) — This wraps the save in a protected call. If something goes wrong (network error, timeout), the game doesn't crash. Instead, success is false and errorMessage tells you what happened.
  • SetAsync(key, data) — Saves the data. Async means it happens in the background so your game doesn't freeze.
💡 Tip — Always use pcall with DataStore calls. Networks fail sometimes, and your game should handle it gracefully.

Loading Player Data on Join

When a player joins, load their saved data. Use the PlayerAdded event, also in a Script in ServerScriptService:

Basic Load Script (ServerScriptService)
local DataStoreService = game:GetService("DataStoreService")
local playerDataStore = DataStoreService:GetDataStore("PlayerData")

local Players = game:GetService("Players")

Players.PlayerAdded:Connect(function(player)
    local key = "Player_" .. player.UserId
    
    -- Create value objects to hold the data
    local levelValue = Instance.new("IntValue")
    levelValue.Name = "Level"
    levelValue.Parent = player
    
    local coinsValue = Instance.new("IntValue")
    coinsValue.Name = "Coins"
    coinsValue.Parent = player
    
    -- Load the data
    local success, data = pcall(function()
        return playerDataStore:GetAsync(key)
    end)
    
    if success and data then
        levelValue.Value = data.level
        coinsValue.Value = data.coins
        print("Loaded data for " .. player.Name)
    else
        -- First-time player (no saved data)
        levelValue.Value = 1
        coinsValue.Value = 0
        print("New player: " .. player.Name)
    end
end)

Here's what happens:

  • We create IntValue objects to hold the player's level and coins. These live inside the player object so other scripts can easily access them.
  • GetAsync(key) retrieves the saved data. If the player joined for the first time, data is nil.
  • If data exists, we restore their level and coins. If not, they start fresh with level 1 and 0 coins.

Complete DataStore System

Putting it all together, here's a production-ready script you can paste into ServerScriptService:

Full DataStore Handler (ServerScriptService)
local DataStoreService = game:GetService("DataStoreService")
local playerDataStore = DataStoreService:GetDataStore("PlayerData")
local Players = game:GetService("Players")

-- Load data when player joins
Players.PlayerAdded:Connect(function(player)
    local key = "Player_" .. player.UserId
    
    -- Create value objects
    local levelValue = Instance.new("IntValue")
    levelValue.Name = "Level"
    levelValue.Parent = player
    
    local coinsValue = Instance.new("IntValue")
    coinsValue.Name = "Coins"
    coinsValue.Parent = player
    
    -- Load saved data
    local success, data = pcall(function()
        return playerDataStore:GetAsync(key)
    end)
    
    if success and data then
        levelValue.Value = data.level
        coinsValue.Value = data.coins
        print("Loaded data for " .. player.Name)
    else
        levelValue.Value = 1
        coinsValue.Value = 0
        print("New player: " .. player.Name)
    end
end)

-- Save data when player leaves
Players.PlayerRemoving:Connect(function(player)
    local key = "Player_" .. player.UserId
    local data = {
        level = player:FindFirstChild("Level").Value,
        coins = player:FindFirstChild("Coins").Value
    }
    
    local success, errorMessage = pcall(function()
        playerDataStore:SetAsync(key, data)
    end)
    
    if success then
        print("Saved data for " .. player.Name)
    else
        print("Failed to save data: " .. errorMessage)
    end
end)

This single script handles both loading and saving. Place it in ServerScriptService and it runs automatically.

Updating Data During Play

You don't save every single change to DataStore — that's too slow. Instead, update the IntValue in the player, and save everything when they leave. Here's how other scripts change the data:

Example: Add Coins (any Script in game)
local player = game.Players:WaitForChild("PlayerName") -- or however you find the player
local coinsValue = player:WaitForChild("Coins")

coinsValue.Value = coinsValue.Value + 50
print("Player now has " .. coinsValue.Value .. " coins")

The value updates instantly for the player to see. When they leave, the DataStore save script automatically persists it.

💡 Tip — For rapid, frequent updates (like position tracking), consider debouncing saves or writing only every 30 seconds instead of on every change. This keeps your game fast.

Error Handling Best Practices

Network issues happen. Here's how to handle them without breaking your game:

Robust Save with Retries
local function savePlayerDataWithRetry(player, maxRetries)
    maxRetries = maxRetries or 3
    local key = "Player_" .. player.UserId
    local data = {
        level = player:FindFirstChild("Level").Value,
        coins = player:FindFirstChild("Coins").Value
    }
    
    for attempt = 1, maxRetries do
        local success, errorMessage = pcall(function()
            playerDataStore:SetAsync(key, data)
        end)
        
        if success then
            print("Saved data for " .. player.Name)
            return true
        else
            print("Save attempt " .. attempt .. " failed: " .. errorMessage)
            if attempt < maxRetries then
                wait(2) -- wait 2 seconds before retrying
            end
        end
    end
    
    print("Failed to save data for " .. player.Name .. " after " .. maxRetries .. " attempts")
    return false
end

This tries up to 3 times before giving up, with a 2-second delay between tries. It gives the network a chance to recover.

Testing DataStores Locally

When you test in Studio on your own computer, DataStore works but data resets when you stop the game. This is intentional — it prevents test data from clogging your real database.

To test properly:

  1. Run your game in Studio.
  2. Join as a player and earn some coins or progress.
  3. Leave the game.
  4. Check the output console — you should see "Saved data for [YourName]".
  5. Join again — you should see "Loaded data for [YourName]".

If you see errors, read them carefully. They usually tell you exactly what went wrong (permissions, network, bad key format, etc.).

DataStore Tips & Tricks

Use descriptive key names. Instead of just player.UserId, use "Player_" .. player.UserId .. "_v1". If you ever need to change your data structure, the version number helps you keep old and new data separate.

Save on a timer, not every change. If you're tracking position or health, don't save 60 times per second. Instead, auto-save every 30 seconds or when something major happens (level up, quest complete).

Compress large data. If you're storing a huge table with hundreds of items, consider using game:GetService("HttpService"):JSONEncode() and JSONDecode() to serialize it safely.

Test your load logic. What happens if the save file is corrupted or doesn't exist? Your code should gracefully handle it, not crash.

💡 Tip — Always back up important player data offline if you're running a serious game. DataStore is reliable, but nothing is 100% guaranteed.