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:

  1. Go to File → Game Settings.
  2. Click the Security tab.
  3. 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.

💡 Tip — Always test data persistence in Studio mode before publishing. Use a test account and leave/rejoin to verify saves work.

Basic DataStore Structure

Here's a complete script for saving and loading player data. Place this in ServerScriptService:

Basic DataStore Script (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.
  • pcall wraps 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:

Retry Logic for Saving Data
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:

Auto-Save with Rate Limiting
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.

💡 Tip — Use 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:

Data Validation on Load
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:

  1. Enable Studio Access to API Services (see earlier section).
  2. Run your game in Studio with Run or Run as Client.
  3. Open the Command Bar at the bottom of Studio.
  4. Use task.wait() to pause, then type commands to check data.

To manually check saved data:

Debug Command (in Command Bar)
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.

💡 Tip — Use a test alt account to generate real UserId saves. You can then query those saves while testing different game versions.

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.