sentinelcore/roblox-skills

roblox-datastores

Use when implementing player data persistence in Roblox, saving/loading player stats or inventory, building leaderboards with ordered datastores, handling data migration between versions, diagnosing data loss issues, or adding auto-save and shutdown-safe data handling with DataStoreService.

First seen Feb 23, 2026

Installation

$ npx skills add sentinelcore/roblox-skills --skill roblox-datastores

Summary

Use when implementing player data persistence in Roblox, saving/loading player stats or inventory, building leaderboards with ordered datastores, handling data migration between versions, diagnosing data loss issues, or adding auto-save and shutdown-safe data handling with DataStoreService.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from sentinelcore/roblox-skills.

npx skills add sentinelcore/roblox-skills

Browse all from sentinelcore/roblox-skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 15
Default branch main
Open issues 0
Status Active

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 6,339 B
  • docs SUMMARY.md 3,621 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 476 installs

SKILL.md

roblox-datastores

Reference for Roblox DataStoreService — saving, loading, and managing player data on the server.

Quick Reference

Method Signature Notes
GetDataStore DSS:GetDataStore(name, scope?) Returns a GlobalDataStore
GetOrderedDataStore DSS:GetOrderedDataStore(name, scope?) For leaderboards
GetAsync store:GetAsync(key) Returns value or nil
SetAsync store:SetAsync(key, value) No return value needed
UpdateAsync store:UpdateAsync(key, fn) Atomic read-modify-write
RemoveAsync store:RemoveAsync(key) Deletes key, returns old value
GetSortedAsync orderedStore:GetSortedAsync(asc, pageSize) Returns DataStorePages

Basic Setup

-- Server Script (ServerScriptService)
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")

local playerStore = DataStoreService:GetDataStore("PlayerData_v1")

local DEFAULT_DATA = {
    coins = 0,
    level = 1,
    xp = 0,
}

Loading Data (GetAsync + pcall)

Always wrap datastore calls in pcall. They can fail due to network issues or rate limits.

local function loadData(player)
    local key = "player_" .. player.UserId
    local success, data = pcall(function()
        return playerStore:GetAsync(key)
    end)

    if success then
        local result = {}
        for k, v in pairs(DEFAULT_DATA) do result[k] = v end
        if data then
            for k, v in pairs(data) do result[k] = v end
        end
        return result
    else
        warn("Failed to load data for", player.Name, ":", data)
        return nil -- signal failure; do not give default data silently
    end
end

Saving Data (SetAsync vs UpdateAsync)

Use SetAsync for simple overwrites. Use UpdateAsync when the value must be based on the current stored value (e.g., incrementing a counter safely across servers).

-- Simple save
local function saveData(player, data)
    local key = "player_" .. player.UserId
    local success, err = pcall(function()
        playerStore:SetAsync(key, data)
    end)
    if not success then
        warn("Failed to save data for", player.Name, ":", err)
    end
end

-- Atomic increment with UpdateAsync
local function addCoinsAtomic(userId, amount)
    local key = "player_" .. userId
    pcall(function()
        playerStore:UpdateAsync(key, function(current)
            current = current or { coins = 0 }
            current.coins = current.coins + amount
            return current
        end)
    end)
end

Retry Logic

local MAX_RETRIES = 3
local RETRY_DELAY = 2

local function safeGet(store, key)
    for attempt = 1, MAX_RETRIES do
        local success, result = pcall(function()
            return store:GetAsync(key)
        end)
        if success then return true, result end
        warn(string.format("GetAsync attempt %d/%d failed: %s", attempt, MAX_RETRIES, result))
        if attempt < MAX_RETRIES then task.wait(RETRY_DELAY) end
    end
    return false, nil
end

Auto-Save: PlayerRemoving + BindToClose

Server shutdown without BindToClose silently discards unsaved data.

local sessionData = {} -- [userId] = data table

Players.PlayerAdded:Connect(function(player)
    local data = loadData(player)
    if data then
        sessionData[player.UserId] = data
    else
        player:Kick("Could not load your data. Please rejoin.")
    end
end)

Players.PlayerRemoving:Connect(function(player)
    local data = sessionData[player.UserId]
    if data then
        saveData(player, data)
        sessionData[player.UserId] = nil
    end
end)

-- Flush all sessions on server shutdown
game:BindToClose(function()
    for userId, data in pairs(sessionData) do
        local key = "player_" .. userId
        pcall(function()
            playerStore:SetAsync(key, data)
        end)
    end
end)

Ordered DataStores (Leaderboards)

Values must be positive integers.

local coinsLeaderboard = DataStoreService:GetOrderedDataStore("Coins_v1")

local function setLeaderboardScore(userId, coins)
    pcall(function()
        coinsLeaderboard:SetAsync("player_" .. userId, math.floor(coins))
    end)
end

local function getTopPlayers(count)
    local success, pages = pcall(function()
        return coinsLeaderboard:GetSortedAsync(false, count) -- false = descending
    end)
    if not success then return {} end

    local results = {}
    for rank, entry in ipairs(pages:GetCurrentPage()) do
        table.insert(results, { rank = rank, userId = entry.key, score = entry.value })
    end
    return results
end

Data Versioning / Migration

Include a _version field and migrate in the load path.

local CURRENT_VERSION = 2

local function migrateData(data)
    local version = data._version or 1
    if version < 2 then
        data.coins = data.gold or 0  -- renamed field
        data.gold = nil
        data._version = 2
    end
    return data
end

Use a versioned datastore name (PlayerData_v2) for breaking schema changes.


Common Mistakes

Mistake Consequence Fix
No pcall around datastore calls Unhandled error crashes the script Always wrap in pcall
Saving on every Changed event Hits rate limits (60 + numPlayers×10 writes/min) Throttle; save on remove + periodic interval
No BindToClose handler Data lost on server shutdown Always flush all sessions in BindToClose
Giving default data on load failure Player silently loses progress Return nil on failure; kick or retry
SetAsync for atomic counters Race condition across servers Use UpdateAsync for read-modify-write
Storing Instances or functions Data silently drops Store only strings, numbers, booleans, plain tables
Reusing datastore name after schema change Old shape clashes with new code Append v2, v3 to name on breaking changes