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
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.
- 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.
- In the test copy, open File > Experience Settings and go to Security.
- 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.
| Type | Saves? | Notes |
|---|---|---|
| Numbers, strings, booleans, buffers | Yes | Strings must be valid UTF-8. Do not store inf or nan. |
| Tables | Yes | If 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, Color3 | No | The 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
| Method | What it does | Use 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
nilcancels 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.
- In the Explorer, hover over ServerScriptService, click + and insert a ModuleScript. Rename it
PlayerDataand replace its contents with the code below. - A ModuleScript only runs when a script requires it. Insert a Script in ServerScriptService named
Mainwith this line:local PlayerData = require(game:GetService("ServerScriptService").PlayerData). Other server scripts require it the same way. - Edit
DEFAULTSand theDatatype to match your game, keeping every field a number, string, boolean or table.
ServerScriptService
PlayerData ModuleScript new
Main Script new
StarterPlayer
StarterPlayerScripts
DataWarning LocalScript new
-- 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
| When | Why |
|---|---|
| Every 180 seconds while the player is in the server | Limits 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:
| Call | What 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.Loaded | Fires 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.
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.
- 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) - 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 loadwarning. - Press Stop. Ending the test closes the server, and the module saves you before it closes.
- 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.
- 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. - 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 happens | Result | Prevention |
|---|---|---|
| A load fails and the script saves anyway | Default values overwrite real progress: a full wipe | Never save a player whose load failed |
Saving only in PlayerRemoving | A crash or shutdown skips the save | Autosave, plus BindToClose that waits for its saves |
No pcall or no retry | One failed request stops the script or drops the save | pcall with a few spaced-out retries |
| Saving on every change | Requests queue; once a queue holds 30, new ones are dropped | Keep data in memory and save on a timer |
| Saving something a data store cannot hold | The save errors, or the value comes back nil | Save numbers, strings, booleans and tables of them |
| Changing the data's shape without a migration | Old saves load into the wrong fields | A schema version and a migration step |
| Two scripts saving the same key | Each save overwrites the other's fields | One module owns the key |
| Two servers holding the same player | The server with older data saves last and overwrites newer progress | Session 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.
| Option | What it is | Worth knowing |
|---|---|---|
| This guide's module | Loading, caching, retries, autosave, shutdown saves and schema versions, without locking | Short enough to read in full and change |
| Roblox's player data and purchasing sample | Reference code from Roblox with session locking, retries processed in order per key, and purchase receipt handling | Roblox says it has not been proven over time in a popular game and should not be used as is without extensive testing |
| ProfileStore | A free, open-source community module, the successor to ProfileService, built around session locking and autosaving | Not 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.
| Request type | Standard data stores | Ordered data stores |
|---|---|---|
Read: GetAsync, the read in UpdateAsync | 60 + 40 × players | 60 + 40 × players |
Write: SetAsync, IncrementAsync, the write in UpdateAsync | 60 + 40 × players | 30 + 5 × players |
List: ListKeysAsync, or GetSortedAsync for ordered stores | 5 + 2 × players | 5 + 2 × players |
Remove: RemoveAsync | 60 + 40 × players | 30 + 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.
| Limit | Value |
|---|---|
| Data store name, key name, scope | 50 characters each |
| Value stored in one key | 4,194,304 characters, measured as JSON |
| Reads from one key, across all servers | 25 MB per minute |
| Writes to one key, across all servers | 4 MB per minute, each request rounded up to the next KB |
| Waiting requests | 30 per queue; beyond that, requests fail with errors 301 to 306 |
| Storage for the whole game | 500 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.
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.
- 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
- Roblox Creator Docs: Data stores. Studio access, GetAsync, SetAsync, UpdateAsync, set vs update
- Roblox Creator Docs: Best practices for data stores. one key per player, buffering in memory, retries with backoff, third-party modules
- Roblox Creator Docs: Data store error codes and limits. error codes, server and game request limits, size, throughput and storage limits
- Roblox Creator Docs: Versioning, listing and caching. hourly versions, snapshots, the 4-second cache, what can be saved
- Roblox Creator Docs: Implement player data and purchasing systems. the reference sample, session locking, failed loads, the 180-second autosave
- Roblox Creator Docs: Data Stores Manager. viewing keys, reverting versions, deleting data
- Roblox Engine API: DataModel:BindToClose. the 30-second shutdown window
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.