Tutorial · Player data

How to save player data in Roblox Studio with DataStores

Roblox keeps nothing about a player between sessions unless a server script saves it. This guide builds a PlayerData module that loads, caches and saves each player's data with DataStoreService, then covers testing, limits and how saved data gets lost.

Updated 30 September 202617 min readIntermediateLuau

In short

Use DataStoreService from a server Script: load each player's data with GetAsync when they join, keep it in a table while they play, and save it with UpdateAsync on a timer, when they leave (PlayerRemoving) and when the server shuts down (game:BindToClose). Wrap every call in pcall and retry failures a few times.

  • To test in Studio, publish the place and turn on File > Experience Settings > Security > Enable Studio Access to API Services, in a separate test game: Studio uses the same data as the live servers.
  • If a player's data fails to load, never save that player. Saving the defaults would overwrite their real progress.
  • Save one table per player under one key such as User_12345, on a timer rather than on every change.
  • For trading or paid items, add session locking so two servers cannot both save the same player.
On this page
  1. Enable Studio access
  2. How data stores work
  3. GetAsync vs UpdateAsync
  4. 1. PlayerData module
  5. How the module works
  6. 2. Use the data
  7. 3. Warn the player
  8. 4. Test in Studio
  9. Why data gets lost
  10. Session locking
  11. Limits
  12. Versions and backups
  13. Common errors
  14. With RoCode
  15. Questions
  16. Sources

Before you start: turn on Studio access in a test copy

A game running in Studio cannot reach data stores until you allow it. Allowing it has a catch that Roblox's docs point out: Studio then reads and writes the same data stores as the live game, so a test that resets your coins in Studio resets them in the live game too, and a bug in test code can damage real players' keys.

  1. Make a test copy. Save your place to a file, open the copy, choose File > Publish to Roblox As and click Create new game. Do not add it as a place inside your live game: every place in one game shares the same data stores.
  2. In the test copy, open File > Experience Settings and go to Security.
  3. Turn on Enable Studio Access to API Services and click Save.

Data store calls only work in server Scripts; a LocalScript that tries gets an error, so clients cannot write saved data. When a button should change it, the client asks the server with a RemoteEvent and the server decides (see the RemoteEvents guide).

How data stores hold player data

A data store is a named set of keys and values on Roblox's servers, shared by every server and every place in your game. DataStoreService:GetDataStore("PlayerData") returns one.

Roblox's best practices come down to one data store for player data, one key per player, one table in each key. Build the key from the player's UserId, such as User_12345, never from their name, which they can change. A fixed User_{UserId} pattern also lets Roblox's automated right-to-be-forgotten processing find a player's data if you set it up.

What a data store can save
TypeSaves?Notes
Numbers, strings, booleans, buffersYesStrings must be valid UTF-8. Do not store inf or nan.
TablesYesIf everything inside can be saved too. A table whose number keys do not start at 1 (so its length is 0) comes back with those keys as strings.
Instances, Vector3, CFrame, Color3NoThe save errors or stores nil. Save an item's name, or a position as three numbers.

Every request is a web call that can fail, takes time and counts against a per-minute budget. So reliable save systems read once when the player joins, keep the data in a server table while they play, and write it back now and then.

GetAsync, SetAsync or UpdateAsync: which to use

The main data store methods
MethodWhat it doesUse it for
GetAsync(key)Reads the value. The result is cached on that server for 4 seconds.Loading a player's data
SetAsync(key, value)Overwrites the value without reading it. One write.A new key, or a value that does not depend on the old one
UpdateAsync(key, callback)Reads the latest value, passes it to your callback and writes what it returns. One read and one write.Saving player data, and any key more than one server can write
IncrementAsync(key, delta)Adds a whole number to a stored whole number.Counters
RemoveAsync(key)Deletes the key and returns the old value. Earlier versions can still be listed and read.Deleting a player's data

SetAsync writes blindly: if two servers save the same key at nearly the same moment, the later write wins, whatever it contains. UpdateAsync reads the current value first, and if another server writes in between, Roblox runs your callback again with the new value. Roblox recommends it whenever a write depends on the current value or several servers may write the key, which describes player data. The callback has three rules:

  • It must not yield: no task.wait() and no other data store calls inside it.
  • It can run more than once, so it should only work out the new value, never award items or fire events.
  • Returning nil cancels the write. The module uses this to avoid overwriting data saved by a newer version of the game.

Step 1: Create the PlayerData module

This ModuleScript is the only code that talks to the data store. It loads each player's data when they join, keeps it in memory, saves it every 180 seconds, when they leave and when the server shuts down, and refuses to save anyone whose data failed to load.

  1. In the Explorer, hover over ServerScriptService, click + and insert a ModuleScript. Rename it PlayerData and replace its contents with the code below.
  2. A ModuleScript only runs when a script requires it. Insert a Script in ServerScriptService named Main with this line: local PlayerData = require(game:GetService("ServerScriptService").PlayerData). Other server scripts require it the same way.
  3. Edit DEFAULTS and the Data type to match your game, keeping every field a number, string, boolean or table.
Explorer after Steps 1 to 3
ServerScriptService
    PlayerData ModuleScript new
    Main Script new
StarterPlayer
    StarterPlayerScripts
        DataWarning LocalScript new
ModuleScript in ServerScriptService named PlayerData
-- PlayerData: loads each player's data when they join, keeps it in memory,
-- and saves it on a timer, when they leave and when the server shuts down.
-- Other server scripts use PlayerData.Get(player) to read and change it.

local Players = game:GetService("Players")
local DataStoreService = game:GetService("DataStoreService")

local STORE_NAME = "PlayerData"
local SCHEMA_VERSION = 2 -- raise this when the shape of the data changes, and add a step to migrate()
local AUTOSAVE_SECONDS = 180
local MAX_ATTEMPTS = 4
local FIRST_RETRY_SECONDS = 1 -- doubles after each failure: 1, 2, 4

export type Data = {
	SchemaVersion: number,
	Coins: number,
	Level: number,
	Inventory: { string },
}

-- What a brand new player starts with
local DEFAULTS: { [string]: any } = {
	Coins = 0,
	Level = 1,
	Inventory = {},
}

type Session = {
	key: string,
	userId: number,
	data: Data,
	canSave: boolean, -- false if the load failed: default data must never replace real progress
}

local store = DataStoreService:GetDataStore(STORE_NAME)
local sessions: { [Player]: Session } = {}
local pendingSaves: { [string]: number } = {} -- saves still running, per key
local loadedEvent = Instance.new("BindableEvent")

local PlayerData = {}

-- Fires with the player once their data is ready. Call PlayerData.Get(player) for the table:
-- a table sent through a BindableEvent arrives as a copy, so changing it would change nothing.
PlayerData.Loaded = loadedEvent.Event

local function deepCopy(value: any): any
	if type(value) ~= "table" then
		return value
	end
	local copy = {}
	for key, item in value do
		copy[key] = deepCopy(item)
	end
	return copy
end

local function defaultData(): Data
	local data = deepCopy(DEFAULTS) -- a fresh copy, so players never share one Inventory table
	data.SchemaVersion = SCHEMA_VERSION
	return data
end

-- Brings saved data up to the current schema. Returns nil if it cannot be used safely.
local function migrate(saved: any): Data?
	if type(saved) ~= "table" then
		return nil
	end
	local version = tonumber(saved.SchemaVersion) or 1
	if version > SCHEMA_VERSION then
		return nil -- saved by a newer version of the game: never load or overwrite it here
	end

	-- One step per schema change, oldest first
	if version < 2 then
		-- Example: version 1 called the currency Gold. Only copy it when Coins is missing,
		-- so older data that already has Coins but no SchemaVersion keeps its value.
		if saved.Coins == nil then
			saved.Coins = saved.Gold
		end
		saved.Gold = nil
	end

	-- Add fields created since this player last saved, and replace values of the wrong type
	for field, default in DEFAULTS do
		if type(saved[field]) ~= type(default) then
			saved[field] = deepCopy(default)
		end
	end
	saved.SchemaVersion = SCHEMA_VERSION
	return saved
end

-- Runs a data store call in pcall. On failure it waits and tries again, doubling the
-- wait each time and adding a little randomness so servers do not all retry together.
local function withRetries(label: string, call: () -> any): (boolean, any)
	for attempt = 1, MAX_ATTEMPTS do
		local ok, result = pcall(call)
		if ok then
			return true, result
		end
		warn(`{label} failed (attempt {attempt} of {MAX_ATTEMPTS}): {result}`)
		if attempt < MAX_ATTEMPTS then
			local waitTime = FIRST_RETRY_SECONDS * 2 ^ (attempt - 1)
			task.wait(waitTime + math.random() * waitTime / 2)
		end
	end
	return false, nil
end

local function save(session: Session): boolean
	if not session.canSave then
		return false
	end
	local key = session.key
	pendingSaves[key] = (pendingSaves[key] or 0) + 1

	local newerSchema = false
	local ok = withRetries(`Save {key}`, function()
		return store:UpdateAsync(key, function(current: any)
			if type(current) == "table" and (tonumber(current.SchemaVersion) or 1) > SCHEMA_VERSION then
				newerSchema = true
				return nil -- returning nil cancels the write
			end
			-- Write the whole current table, tagged with the player's UserId
			return session.data, { session.userId }
		end)
	end)

	local left = (pendingSaves[key] or 1) - 1
	if left > 0 then
		pendingSaves[key] = left
	else
		pendingSaves[key] = nil
	end
	if newerSchema then
		session.canSave = false
		warn(`{key} was saved by a newer version of the game, so this server stopped saving it`)
	end
	return ok and not newerSchema
end

local function startAutosave(player: Player, session: Session)
	task.spawn(function()
		-- Start at a random point in the interval so servers do not all save at once
		task.wait(math.random() * AUTOSAVE_SECONDS)
		while sessions[player] == session do
			save(session)
			task.wait(AUTOSAVE_SECONDS)
		end
	end)
end

local function load(player: Player)
	local key = "User_" .. player.UserId

	-- If this player just left this server and came back, let the last save finish first
	while pendingSaves[key] do
		task.wait()
	end

	local ok, saved = withRetries(`Load {key}`, function()
		return store:GetAsync(key)
	end)
	if player.Parent ~= Players then
		return -- left while loading: nothing has changed, so there is nothing to save
	end

	local data: Data? = nil
	if ok then
		data = if saved == nil then defaultData() else migrate(saved)
	end

	local session: Session = {
		key = key,
		userId = player.UserId,
		data = data or defaultData(),
		canSave = data ~= nil,
	}
	if not session.canSave then
		warn(`Could not load {key}. {player.Name} is playing on default data that will not be saved.`)
	end

	sessions[player] = session
	player:SetAttribute("DataLoaded", session.canSave) -- a GUI can read this and warn the player
	startAutosave(player, session)
	loadedEvent:Fire(player)
end

local function endSession(player: Player)
	local session = sessions[player]
	if not session then
		return
	end
	sessions[player] = nil -- stops the autosave loop and makes a second call do nothing
	save(session)
end

-- The player's live data table, or nil until it has loaded. Change it from server scripts only.
function PlayerData.Get(player: Player): Data?
	local session = sessions[player]
	return if session then session.data else nil
end

-- True when the player's data loaded and will be saved. Check it before selling anything.
function PlayerData.CanSave(player: Player): boolean
	local session = sessions[player]
	return session ~= nil and session.canSave
end

-- Saves now instead of waiting for the timer, for moments that matter such as a purchase.
-- Yields, and returns true once the save has gone through.
function PlayerData.SaveNow(player: Player): boolean
	local session = sessions[player]
	return session ~= nil and save(session)
end

Players.PlayerAdded:Connect(load)
Players.PlayerRemoving:Connect(endSession)
for _, player in Players:GetPlayers() do
	task.spawn(load, player) -- players who joined before this module was first required
end

game:BindToClose(function()
	-- The server gives these functions 30 seconds, so save everyone at the same time
	for player in sessions do
		task.spawn(endSession, player)
	end
	-- Then wait for every save still running, including ones PlayerRemoving started
	while next(pendingSaves) do
		task.wait()
	end
end)

return PlayerData

Once the game is live, keep STORE_NAME and the User_ key pattern the same. Changing either points the module at empty keys, and every player starts again from the defaults.

How the module keeps data safe

Defaults and a schema version

A new player gets a fresh copy of DEFAULTS. It must be a copy: if every player shared one Inventory table, an item added for one would appear for all.

Every saved table carries a SchemaVersion. Adding a field needs no version change, because migrate fills missing fields from the defaults. Renaming or restructuring does: raise SCHEMA_VERSION and add a step, like the example that renames Gold to Coins (in a new game, delete it and start at 1). Data with a higher version than the server's comes from a newer release still rolling out, so older servers leave it alone.

A failed load is never saved

This rule prevents full wipes. If every load attempt fails, the player plays on default data, but canSave is false, so those defaults never overwrite their real progress. The module also sets a DataLoaded attribute to false, which Step 3 uses to tell them. Roblox's reference sample handles failed loads the same way and suggests it over kicking; if you prefer to kick, call player:Kick() with a message where the module warns.

Retries with backoff

withRetries wraps every call in pcall. After a failure it waits 1, then 2, then 4 seconds, each plus up to half as long again at random, and stops after four attempts. The randomness spreads out retries from many servers after an outage, and the cap keeps the waiting to about 10 seconds, inside the 30 seconds a server gets to shut down. Because every save writes the whole live table through UpdateAsync, a retry that finishes late cannot put old data back.

When it saves

WhenWhy
Every 180 seconds while the player is in the serverLimits the loss from a crash. The first save comes at a random point in the interval so servers do not all save at once. Roblox's reference sample also uses 180 seconds.
When the player leaves (PlayerRemoving)The normal final save. The session is removed first, so autosave stops.
When the server shuts down (game:BindToClose)Everyone still in the server is saved at once, and the function waits for every save in flight. Roblox gives it 30 seconds.
On demand, with PlayerData.SaveNow(player)After something that must not be lost, such as a purchase.

If a player rejoins the same server while their final save is still running, the new load waits for that save to finish, so it cannot read the older value.

Step 2: Read and change data from other scripts

Every server script that requires the module shares the same tables:

CallWhat it does
PlayerData.Get(player)The player's live data table, or nil until it has loaded. Change fields directly, such as data.Coins += 10, and the next save picks it up.
PlayerData.LoadedFires with the player once their data is ready, including after a failed load. It passes only the player, because a table sent through a BindableEvent arrives as a copy.
PlayerData.CanSave(player)false if the load failed. Check it before selling anything: a purchase that cannot be saved is gone when the player leaves.
PlayerData.SaveNow(player)Saves at once and returns true if it worked. It yields.

The module is the one place saved data lives, and leaderstats is only a display: build it as in the leaderboard guide, fill it from PlayerData.Get(player) in a Loaded handler, and update both when a value changes. Never read saved values back out of leaderstats, and never let a second script save the same key.

A script that requires the module after players have joined misses Loaded for them. Connect the handler, then also loop over Players:GetPlayers() and handle anyone for whom PlayerData.Get(player) already returns a table.

Replacing an older save script, such as the one in the leaderboard guide? Its values stay under its own data store and key, so this module starts everyone from the defaults. Before launch, either point STORE_NAME and the User_ prefix at the old ones (migrate treats data without a SchemaVersion as version 1 and fills in missing fields), or copy the old values across, then remove the old script.

Clients never change this data directly. A shop button fires a RemoteEvent; the server checks CanSave, compares the price with the player's Coins, changes the table and calls SaveNow. The shop GUI guide builds that server-side purchase flow.

Step 3: Tell the player when their data did not load

A player whose data failed to load should know this session will not be saved, so they can rejoin instead of playing for nothing. This LocalScript watches the DataLoaded attribute and shows a banner when it is false.

LocalScript in StarterPlayerScripts named DataWarning
local Players = game:GetService("Players")

local player = Players.LocalPlayer
local shown = false

local function showWarning()
	if shown or player:GetAttribute("DataLoaded") ~= false then
		return
	end
	shown = true

	local gui = Instance.new("ScreenGui")
	gui.Name = "DataWarning"
	gui.ResetOnSpawn = false

	local label = Instance.new("TextLabel")
	label.AnchorPoint = Vector2.new(0.5, 0)
	label.Position = UDim2.new(0.5, 0, 0, 12)
	label.Size = UDim2.new(0.8, 0, 0, 48)
	label.BackgroundColor3 = Color3.fromRGB(110, 28, 28)
	label.TextColor3 = Color3.new(1, 1, 1)
	label.TextScaled = true
	label.Text = "Your saved progress could not be loaded. Nothing you do in this server will be saved, so rejoin later."
	label.Parent = gui

	gui.Parent = player:WaitForChild("PlayerGui")
end

-- The server sets DataLoaded once, after it tries to load this player's data
player:GetAttributeChangedSignal("DataLoaded"):Connect(showWarning)
showWarning()

Step 4: Test saving in Studio

Do this in the test copy, with Studio access turned on.

  1. Give yourself something to save. Below the require line in Main, add: PlayerData.Loaded:Connect(function(player) local data = PlayerData.Get(player) if data then data.Coins += 100 print(player.Name, data.Coins) end end)
  2. Press Play. The Output window (in the Window menu, or on the Script tab) shows your name and 100 (on a first run), with no Could not load warning.
  3. Press Stop. Ending the test closes the server, and the module saves you before it closes.
  4. Press Play again. The Output should show 200, 100 more than last time. If it shows the same number again, the save did not go through: read the warnings above it.
  5. Test the failure path. Turn Studio access off and press Play. After several seconds of retries the Output shows Could not load User_... and 100, and the banner from Step 3 appears. Stop, turn access back on and Play: the Output shows 300, your saved 200 plus 100, because the failed session was never saved over your data.
  6. Remove the test line from Main.

Why player data gets lost

These are the usual ways a save system loses progress. The module handles the first four rows and the schema row in code; the others depend on what you store and how you use it, and the last needs session locking.

What happensResultPrevention
A load fails and the script saves anywayDefault values overwrite real progress: a full wipeNever save a player whose load failed
Saving only in PlayerRemovingA crash or shutdown skips the saveAutosave, plus BindToClose that waits for its saves
No pcall or no retryOne failed request stops the script or drops the savepcall with a few spaced-out retries
Saving on every changeRequests queue; once a queue holds 30, new ones are droppedKeep data in memory and save on a timer
Saving something a data store cannot holdThe save errors, or the value comes back nilSave numbers, strings, booleans and tables of them
Changing the data's shape without a migrationOld saves load into the wrong fieldsA schema version and a migration step
Two scripts saving the same keyEach save overwrites the other's fieldsOne module owns the key
Two servers holding the same playerThe server with older data saves last and overwrites newer progressSession locking

Session locking, "lock lost" and ProfileStore

A player is connected to one server at a time, but the old server's final save can still be running when they join a new one: a slow retry, a teleport or a quick rejoin. If the new server loads first, it holds older data, and whichever server saves last wins. Roblox's docs note that in games with trading, this gap is a common source of item duplication.

Session locking closes it. When a server loads a player's key, it writes its own lock ID into the key's metadata in the same UpdateAsync call. Any other server that finds an unexpired lock it did not place will not load or save the key. The owner refreshes the lock with each autosave and removes it in the final save; if it stops refreshing, the lock expires and another server may take it.

What "lock lost" means

In a session-locking system, a server has lost the lock when another server now owns the player's key, because the lock expired or was taken over. Its copy may be out of date, so it must stop saving that player; a common response is to kick them with a message so they reconnect. If you saw "lock lost" as a player, it comes from that game's own save system, and only its developers can explain it.

Do you need it?

The module in this guide does not lock sessions. Without locking, the realistic worst case is a player losing what they earned on the old server since its last successful save, in a rare race. Add locking when duplication or loss would cost players something real: trading, developer products, rare items or frequent teleports.

OptionWhat it isWorth knowing
This guide's moduleLoading, caching, retries, autosave, shutdown saves and schema versions, without lockingShort enough to read in full and change
Roblox's player data and purchasing sampleReference code from Roblox with session locking, retries processed in order per key, and purchase receipt handlingRoblox says it has not been proven over time in a popular game and should not be used as is without extensive testing
ProfileStoreA free, open-source community module, the successor to ProfileService, built around session locking and autosavingNot made by Roblox. Moving an existing game to it means migrating the data you already saved

For any third-party module, Roblox advises checking who owns it, whether it is maintained and how you would reach your data without it. The same docs call DataStore2 a legacy library that new games should not use.

Data store limits

These are the limits in Roblox's documentation on 30 September 2026. Roblox revises them, so check the error codes and limits page before designing around an exact number.

Default request limits for each server, per minute
Request typeStandard data storesOrdered data stores
Read: GetAsync, the read in UpdateAsync60 + 40 × players60 + 40 × players
Write: SetAsync, IncrementAsync, the write in UpdateAsync60 + 40 × players30 + 5 × players
List: ListKeysAsync, or GetSortedAsync for ordered stores5 + 2 × players5 + 2 × players
Remove: RemoveAsync60 + 40 × players30 + 5 × players

With 20 players, a server may make 860 standard writes a minute; this module's autosave uses about 7 of them, plus one per player who leaves. Saving on every coin pickup is what runs a budget out. A second budget covers the whole game and is shared with Open Cloud: 300 reads a minute plus 40 per player online, and 300 writes plus 20 per player.

Size and throughput limits
LimitValue
Data store name, key name, scope50 characters each
Value stored in one key4,194,304 characters, measured as JSON
Reads from one key, across all servers25 MB per minute
Writes to one key, across all servers4 MB per minute, each request rounded up to the next KB
Waiting requests30 per queue; beyond that, requests fail with errors 301 to 306
Storage for the whole game500 MB plus 1 MB per lifetime user

Measure a player's data with #HttpService:JSONEncode(data), and see a live server's remaining budget with DataStoreService:GetRequestBudgetForRequestType(Enum.DataStoreRequestType.StandardWrite). GetAsync calls answered from the 4-second cache do not count. Studio's Run mode has its own, possibly lower, limits, so Roblox suggests Team Create for testing rate limits.

Versions and backups

Standard data stores keep history for you. The first write to a key in each UTC hour is kept as a version, and later writes in that hour overwrite each other. An old version expires 30 days after a newer write replaces it; the latest never expires. Ordered data stores keep no versions.

To roll back one player, open Data Stores Manager (Creations, your game, Configure > Data Stores Manager), select their key, pick an earlier version and click Revert, which needs Edit permission. Do it while the player is offline, or the server holding their session will save its copy over the revert. From a server script, ListVersionsAsync and GetVersionAsync find and read an old version, which you then write back as the current value.

Before publishing an update that changes how data is saved, Roblox recommends a snapshot with the Open Cloud Snapshot Data Stores API (one a day per game). Without one, a buggy save at 3:30 UTC overwrites everything written to that key since 3:00, because it falls in the same hourly version.

Common errors and fixes

Data store errors appear in Studio's Output window, and the module prints a warning with Roblox's error text for every failed attempt. The script errors guide explains reading Output in general.

StudioAccessToApisNotAllowed (error 403)

"Can't write to DataStore from Studio because API access is not enabled." Publish the place, then turn on Enable Studio Access to API Services under File > Experience Settings > Security, in the test copy.

DataStore request was added to queue

The server is sending requests faster than its budget, or writing the same key too often, usually by saving on every change or in a loop. Once a queue holds 30, new requests fail with errors 301 to 306. Keep data in memory and save on a timer.

CantStoreValue (error 104), or a field comes back nil

The table holds something a data store cannot save, such as an Instance, Vector3 or Color3. Save a name, ID or plain numbers instead and rebuild the object on load.

ValueTooLarge (error 105)

One key holds more than 4,194,304 characters of JSON. Measure with #HttpService:JSONEncode(data), drop what you can rebuild, or split the data across a few fixed keys.

No errors, but the data does not save

Look for the module's Could not load warning: after a failed load it refuses to save, by design. Otherwise, the value was probably changed in a LocalScript, which never reaches the server, or another script saves the same key.

Data resets every time I rejoin

The key changes between sessions (built from player.Name, or STORE_NAME was edited), the load fails and you see defaults, or nothing requires the module so it never runs. Check the Output and that Main requires PlayerData.

Build it with RoCode

If you would rather not wire this up by hand, RoCode can build it inside the place you have open. It is an independent AI agent, not made by Roblox, that works through a Roblox Studio plugin: it writes Luau that uses DataStoreService like any script and creates the scripts in your Explorer. It does not run them or call your data stores itself.

You type

Add saving to my game: keep Coins, Level and Inventory in a PlayerData module with autosave and shutdown saves, and show Coins in the leaderstats I already have.

RoCode does
  • Can look for existing leaderstats and DataStore code with Search Scripts and Read Script, so it can extend what you have instead of adding a second save script. In a big place, name the script or folder in your message.
  • Creates the ModuleScript and the Script that requires it with Create Script, or changes your existing ones with Edit Script
  • Runs Check Script on each script it wrote: a compile check inside Studio with a short warning list, not a type check or a test run
  • Sends its changes in batches, and each batch is an undo point in Studio

You still turn on Studio Access to API Services in your test copy and press Play yourself: RoCode cannot change that setting and does not start playtests. If a save fails in your test, Check Output can read the Server lines from the playtest you ran. How RoCode connects to Studio.

Questions

How do I enable DataStore in Roblox Studio?

Publish the place, open File > Experience Settings > Security, turn on Enable Studio Access to API Services and click Save. Do it in a separate test game, because Studio then uses the same data stores as that game's live servers.

Does Roblox save player data automatically?

No. Nothing about a player lasts beyond the session unless a server script saves it with DataStoreService, and that includes leaderstats.

Should I use ProfileStore or DataStoreService directly?

ProfileStore is built on data stores too, so the question is whether you want session locking written and maintained for you. For trading, paid items or frequent teleports, locking is worth having, from a library or Roblox's reference sample. For a smaller game, a module like this one is easier to read and change.

How do I reset or delete a player's data?

In Data Stores Manager, find the key and choose Mark for Deletion, which you can undo during a cooldown period, or call RemoveAsync on the key from a server script. Do it while the player is offline, or their server's next save writes the data back. In the test copy, changing STORE_NAME starts everyone fresh; never do that in a live game.

Sources and further reading

How the code was checked: every script on this page passes Luau's strict type checker against Roblox's API definitions (luau-lsp, 30 September 2026). A type check catches misspelt APIs and wrong types, not game logic, so play-test in Studio before you publish.

Try RoCode on your own place.

Free every day, no card required. Install the plugin, describe the feature, and review what lands in Studio.