How To Use, Utilize, and Succeed with DataStores

How To Use, Utilize, and Succeed with Datastores

By Schedency, Founder @ Madison Incorporated

Saving data is an important part of creating your experience. It allows your players to return and continue where they left off, without needing to start over again.

Many of you will most likely use Datastores at one point or another in your journey on Roblox. Many tutorials on YouTube and the Developer Forum cover this topic, but many of them are outdated and use methods that shouldn’t be used today.

I’ll be guiding you step-by-step through this tutorial to help you succeed. If you have any questions about this, let me know in the comments below!


1. Getting Started

First, you need to know what a data store is. A data store in Roblox is a holder that contains data. It can only save digits and is connected to your account through your user ID.

To synchronize and save the data, you can use DataStore:SetAsync() or DataStore:UpdateAsync(). To retrieve data, you’ll use DataStore:GetAsync().

:warning: Hold Up! What is the difference between SetAsync() and UpdateAsync()? :warning:

SeyAsync() gets the latest data changes and overwrites this into our connected data store, while UpdateAsync() retrieves the value and data from our key and then proceeds to change the data store. It’s recommended to use UpdateAsync() rather than SetAsync() because SetAsync() often leads to data losses if the game is immediately shut down or the player randomly disconnects.

You can only access DataStores on the server, such as with Scripts and ModuleScripts. You can not access the service on the client.

There are also limits on how many requests you can send to a data store, however, these are not very common to come across unless you spam :SetAsync() in an infinite loop. You can view these limits here.


2. Setting up in Studio

In ServerScriptService, create a new Script and name it “DataStoreHandling”. We’ll use this script for executing our code.

Next, add a new ModuleScript to the script we just created, and name it “dataModule”. It should look like this now:
image

Now, open the script dataModule.


3. Starting with the Modules

Inside the dataModule script, it’ll look like this:

local module = {}

return module

Rename the variable “module” to “dataModule”, and change it to both of the lines we have. It should look something like this now:

local dataModule = {}

return dataModule

Next, we’re going to be creating 3 functions. These functions will be essential in our saving and loading procedures. We’ll also use type-checking to ensure that we get the right player model for our script.

Create three functions named addInstances, saveData and loadData in the dataModule script. It should look something like this:

function dataModule.addInstances()
    -- // This will add our instances to the player
end

function dataModule.saveData()
   -- // This will save our data upon client disconnect.
end

function dataModule.loadData()
   -- // This will load our data once the client connects to the server
end

Now we’ll use something called type-checking. Type-checking is when we make the script double-check that the value of our requested package is the same.

Type-checking Example

For example, if I wanted to make a for loop with a StringValue that prints out the value of the strings, I can do something like this:

for i, obj: StringValue in ipairs(workspace:GetChildren()) do
   print(obj.Value)
end

We’ll add a player: Player instance to each of the functions, like this:

function dataModule.addInstances(player: Player)
    
end

function dataModule.saveData(player: Player)
   
end

function dataModule.loadData(player: Player)
   
end

4. Going deeper into the Code

Now we’ll go deeper within each function, and create the fundamentals for it. I recommend going through the following paragraphs exactly as outlined below, otherwise you may encounter some problems with your code.

4a. addInstances()

4a. Adding Instances

Firstly, we’ll need some values that we can save our player’s data on. For this tutorial we’re going to be using an IntValue named “Points”, but you can rename it to whatever you’d like.

We’ll start by creating a new folder. This folder is going to be named “Leaderstats”, and we’ll set the value to “leaderstats”. Then, we’ll set the parent of our folder to the player we defined earlier.

:warning:WAIT! Why can’t we name the value of the folder as “Leaderstats”? :warning:

Due to limitations within the Roblox engine, we’re only able to call the Leaderstats function in lowercase. You can bypass this if you don’t want to show it in the leaderboard but in a custom GUI instead.

Your code should then look like this:

function dataModule.saveData(player: Player)
    local Leaderstats = Instance.new("Folder")
    Leaderstats.Name = "leaderstats"
    Leaderstats.Parent = player
end

Now we’re going to be creating a new instance, an “IntValue”. This will be where our points will be stored and visible to the player through the leaderboard. We’ll name the value as “Points”, and set the value to 0.

function dataModule.addInstances(player: Player)
	local Leaderstats = Instance.new("Folder")
	Leaderstats.Name = "leaderstats"
	Leaderstats.Parent = player
	
	local pointsValue = Instance.new("IntValue")
	pointsValue.Name = "Points"
	pointsValue.Value = 0
	pointsValue.Parent = Leaderstats
end
4b. saveData()

4b. Saving the Data

The next thing we’re going to do is make a function that saves our data. We’ll start by making 2 variables, one where we call DataStoreService and one where we create our data store.

You can name your data store variable and value as whatever you’d like, as long as you remember the name of it. For this tutorial, we’re going to name it dataStore and give it the value DataStore.

:warning: Warning :warning:

Make sure to save the name of the value inside the parentheses of the datastore value. This is to ensure that we call the same store every time we call it from a different script or function. Otherwise, we might encounter some problems. You can also call the leaderstats folder here, as we’ll need it further.

Your script should now look like this:

function dataModule.saveData(player: Player)
	local DataStoreService = game:GetService("DataStoreService")
	local dataStore = DataStoreService:GetDataStore("DataStore")
	local leaderstatsFolder = player:FindFirstChild("leaderstats")
end

Next, we’re going to create 3 new variables. One of these will be the key. This is where we define where our data is stored, and in this case, it’s our player’s user ID. Then, we’re going to define the data. This is the value that we want to save in our data store. If you want to save multiple values, see section 7 for a deeper input on that.

Finally, we’re going to create a success, result variable for our pcall. If you don’t understand what a pcall is, or you want to learn more on how to use them, check out this tutorial for a deeper explanation.

After you’ve done this, your code should look like this:

function dataModule.saveData(player: Player)
	local DataStoreService = game:GetService("DataStoreService")
	local dataStore = DataStoreService:GetDataStore("DataStore")
	local leaderstatsFolder = player:FindFirstChild("leaderstats")
	
 -- // New Variables
	local Key = player.UserId
	local data = leaderstatsFolder.Points.Value
	
	local success, result
end

Now we’re going to create a repeat until loop. This means that we want something to repeat a set amount of times until we receive what we want. In this case, we’ll be repeating our data saving until it has been successfully saved. To do this, start by typing repeat in a line of the script, and click enter. The until option will automatically be added once you do this.

function dataModule.saveData(player: Player)
	local DataStoreService = game:GetService("DataStoreService")
	local dataStore = DataStoreService:GetDataStore("DataStore")
	local leaderstatsFolder = player:FindFirstChild("leaderstats")
	
	local Key = player.UserId
	local data = leaderstatsFolder.Points.Value
	
	local success, result

  -- // Our repeat until loop
	repeat

	until
end

After this, we can use the variable we made earlier, success, result, to call out our pcall. Then, we’ll save the data by using the UpdateAsync() function, and then set it to return the data.

After that, we’ll need to add a task.wait() after our pcall, to ensure that the engine waits a few seconds before attempting to save again. Finally, we’ll set the until value to success.

:warning: If we don’t add a task.wait() our program will crash due to the infinite request the server sends to the datastore servers. :warning:

function dataModule.saveData(player: Player)
	local DataStoreService = game:GetService("DataStoreService")
	local dataStore = DataStoreService:GetDataStore("DataStore")
	local leaderstatsFolder = player:FindFirstChild("leaderstats")
	
	local Key = player.UserId
	local data = leaderstatsFolder.Points.Value
	
	local success, result

	repeat
		success, result = pcall(function() -- // Adding our pcall function
			dataStore:UpdateAsync(Key, function() -- Saving the data using UpdateAsync()
				return data
			end)
		end)
		
		task.wait()
	until success -- // If the save is a success, cancel the loop. If not, the loop continues.
end

It’s always a good idea to print out potential errors that occur in our code. In addition, we can print out if the save was a success. To do this, we’ll use a if not, elseif, else check, and then use the print() element to print out the error messages and success.

function dataModule.saveData(player: Player)
	local DataStoreService = game:GetService("DataStoreService")
	local dataStore = DataStoreService:GetDataStore("DataStore")
	local leaderstatsFolder = player:FindFirstChild("leaderstats")
	
	local Key = player.UserId
	local data = leaderstatsFolder.Points.Value
	
	local success, result

	repeat
		success, result = pcall(function()
			dataStore:UpdateAsync(Key, function() 
				return data
			end)
		end)
		
		task.wait()
	until success

   -- // Adding our if-statements to check for errors or success
	if not success then

	elseif success then

	else

	end

end

Then, we can make it so if it is not success, then we use the warn() function to print the error message. If it is success, then we can use print() to print out our success message, and finally, if a different error occurs, we can warn out that error as well.

function dataModule.saveData(player: Player)
	local DataStoreService = game:GetService("DataStoreService")
	local dataStore = DataStoreService:GetDataStore("DataStore")
	local leaderstatsFolder = player:FindFirstChild("leaderstats")
	
	local Key = player.UserId
	local data = leaderstatsFolder.Points.Value
	
	local success, result

	repeat
		success, result = pcall(function()
			dataStore:UpdateAsync(Key, function() 
				return data
			end)
		end)
		
		task.wait()
	until success

	if not success then
		warn("An error occurred while attempting to override DataStore values, see here for more: " .. tostring(result))
	elseif success then
		print("Saved data to user " .. player.Name)
	else
		warn("An error occurred, see for more: " .. tostring(result))
	end

end

:warning: To ensure that the result message can be displayed in a string, we can use the tostring() element to turn our error message into a string that we can connect to our main printing. :warning:

4c. loadData()

4c. Loading the data

It’s probably a good thing to load our player data once the player connects to our server. Loading the data is almost the same procedure as saving the data, but there are a few differences.

First, we can start by creating the same variables we created in our saveData() function. In addition, we’ll add two new elements.

The leaderstats folder from the player, and the IntValue from our folder. This time we’ll also be setting our data variable to nil, as we’ll be defining the data later on.

function dataModule.loadData(player: Player)
	local PlayerService = game:GetService("Players")
	local DataStoreService = game:GetService("DataStoreService")
	local dataStore = DataStoreService:GetDataStore("DataStore")
	
	local Key = player.UserId
	
	local leaderstatsFolder = player:FindFirstChild("leaderstats")
	local pointsValue = leaderstatsFolder:FindFirstChild("Points")
	
	local data
	local success, result
end

Next up we’ll be adding a new repeat until loop. This time we’ll be using GetAsync() instead of UpdateAsync(), as we’re getting our data, and not saving it. We’ll only be using our key to retrieve the data, and we do not connect it to a function.

Like with the saving data, we’ll be setting our until value to success. However, since we’re loading the data, and not just saving it, it can cause a problem if the player suddenly disconnects and the loop throws an error.

To fix this, we’ll simply add a check to ensure that the player is still present. If not, it’ll cancel the loop and the loading process.

function dataModule.loadData(player: Player)
	local PlayerService = game:GetService("Players")
	local DataStoreService = game:GetService("DataStoreService")
	local dataStore = DataStoreService:GetDataStore("DataStore")
	
	local Key = player.UserId
	
	local leaderstatsFolder = player:FindFirstChild("leaderstats")
	local pointsValue = leaderstatsFolder:FindFirstChild("Points")
	
	local data
	local success, result

   -- // Our repeat until loop
	repeat
		success, result = pcall(function()
			data = dataStore:GetAsync(Key) -- // Getting the data from our key
		end)
		
		task.wait()
	until success or not PlayerService:FindFirstChild(player.Name) -- // Continue until loading is successful, or until the player doesn't exist anymore in the player list.
end

Now we’ll be adding an if, elseif not, else check to ensure that if it’s a success, then we print out a success message and set the value to the data. If it’s not a success, we’ll print out the error in a string format, and if any other errors occur, we’ll print that error.

function dataModule.loadData(player: Player)
	local PlayerService = game:GetService("Players")
	local DataStoreService = game:GetService("DataStoreService")
	local dataStore = DataStoreService:GetDataStore("DataStore")
	
	local Key = player.UserId
	
	local leaderstatsFolder = player:FindFirstChild("leaderstats")
	local pointsValue = leaderstatsFolder:FindFirstChild("Points")
	
	local data
	local success, result
	
	repeat
		success, result = pcall(function()
			data = dataStore:GetAsync(Key)
		end)
		
		task.wait()
	until success or not PlayerService:FindFirstChild(player.Name)
	
	if success then
		pointsValue.Value = data
		print("Successfully loaded the data for user " .. player.Name)
	elseif not success then
		warn("An error occurred while trying to load the data, see here for more: " .. tostring(result))
	else
		warn("An error occurred, see here for more: " .. tostring(result))
	end
end
Final Result

Our final code in the dataModule should look like this:

local dataModule = {}

function dataModule.addInstances(player: Player)
	
	local Leaderstats = Instance.new("Folder")
	Leaderstats.Name = "leaderstats"
	Leaderstats.Parent = player
	
	local pointsValue = Instance.new("IntValue")
	pointsValue.Name = "Points"
	pointsValue.Value = 0
	pointsValue.Parent = Leaderstats
	
end

function dataModule.saveData(player: Player)
	
	local DataStoreService = game:GetService("DataStoreService")
	local dataStore = DataStoreService:GetDataStore("DataStore")
	local leaderstatsFolder = player:FindFirstChild("leaderstats")
	
	local Key = player.UserId
	local data = leaderstatsFolder.Points.Value
	
	local success, result
	
	repeat
		
		success, result = pcall(function()
			dataStore:UpdateAsync(Key, function()
				return data
			end)
		end)
		
		task.wait()
	until success
	
	if not success then
		warn("An error occurred while attempting to override DataStore values, see here for more: " .. tostring(result))
	elseif success then
		print("Saved data to user " .. player.Name)
	else
		warn("An error occurred, see for more: " .. tostring(result))
	end
end

function dataModule.loadData(player: Player)
	
	local PlayerService = game:GetService("Players")
	local DataStoreService = game:GetService("DataStoreService")
	local dataStore = DataStoreService:GetDataStore("DataStore")
	
	local Key = player.UserId
	
	local leaderstatsFolder = player:FindFirstChild("leaderstats")
	local pointsValue = leaderstatsFolder:FindFirstChild("Points")
	
	local data
	local success, result
	
	repeat
		success, result = pcall(function()
			data = dataStore:GetAsync(Key)
		end)
		
		task.wait()
	until success or not PlayerService:FindFirstChild(player.Name)
	
	if success then
		pointsValue.Value = data
		print("Successfully loaded the data for user " .. player.Name)
	elseif not success then
		warn("An error occurred while trying to load the data, see here for more: " .. tostring(result))
	else
		warn("An error occurred, see here for more: " .. tostring(result))
	end
end

return dataModule

5. DataStoreHandling Server Script

Now we’ll start connecting our module to our events so that the data can be correctly saved and loaded. We’ll start by creating a variable and using the require() function to retrieve our code.

local dataModule = require(script.dataModule)

Next, we’ll be retrieving our functions from the module. To do this, we can use the module.function method to retrieve it, like this:

local dataModule = require(script.dataModule)

-- // Getting our functions from the module
local addInstances = dataModule.addInstances
local saveData = dataModule.saveData
local loadData = dataModule.loadData

Now we’re going to create 2 events, one PlayerAdded and one PlayerRemoving event. In these, we’ll be connecting our functions to make sure they run effectively and correctly. We’re also going to add the PlayerService service as a variable.

We’ll also be installing the player object as a parameter in each function, to ensure that the code knows which player we’re referring to.

local dataModule = require(script.dataModule)

local addInstances = dataModule.addInstances
local saveData = dataModule.saveData
local loadData = dataModule.loadData

local PlayerService = game:GetService("Players")

-- // Player Added Event
game.Players.PlayerAdded:Connect(function(player: Player)
	addInstances(player)
	loadData(player)
end)

-- // PlayerRemoving Event
game.Players.PlayerRemoving:Connect(function(player: Player)
	saveData(player)
end)

Finally, we’re adding a game:BindToClose() function. In the case of an immediate game shutdown or client disconnection, we want the server to safely save our data before the disconnection happens.

In this function, we’ll be adding a for loop, which will run through every player instance in the server, and then save the data to the corresponding player.

game:BindToClose(function()
	for i, player: Player in ipairs(PlayerService:GetPlayers()) do
		saveData(player)
	end
end)

Our final result for the DataStoreHandling script will be this:

local dataModule = require(script.dataModule)

local addInstances = dataModule.addInstances
local saveData = dataModule.saveData
local loadData = dataModule.loadData

local PlayerService = game:GetService("Players")

game.Players.PlayerAdded:Connect(function(player: Player)
	addInstances(player)
	loadData(player)
end)

game.Players.PlayerRemoving:Connect(function(player: Player)
	saveData(player)
end)

game:BindToClose(function()
	for i, player: Player in ipairs(PlayerService:GetPlayers()) do
		saveData(player)
	end
end)

6. Testing out our script!


7. Adding more values

In this case, if you want to save more than 1 value, we’ll need to make some changes in our script.

Start by creating a new IntValue with its properties in the addInstance() function, like this:

 -- // New IntValue + Properties	
	local moneyValue = Instance.new("IntValue")
	moneyValue.Name = "Money"
	moneyValue.Value = 0
	moneyValue.Parent = Leaderstats

Next, we need to transform our data variable in the saveData() function from a singular value to a table value. We do this by doing data = {}, and then inserting our values through there.

To make a value, call the leaderstatsFolder + Name + Value, like this:

	local data = {
		leaderstatsFolder.Points.Value,
		leaderstatsFolder.Money.Value
	}

:warning: Make sure to do this step for EVERY value you create in your experience. Otherwise, it won’t be saved properly. You don’t need to change anything in the UpdateAsync function. :warning:

Finally, set the data of each value to the corresponding line it represents. Unlike arrays, tables start with 1 and go up. The formula for doing this is obj.Value = data[n].

Since we have two values, we’ll use the numbers 1 and 2, with Points representing 1 and Money representing 2.

	if success then
     -- // Getting our data from our table
		pointsValue.Value = data[1]
		moneyValue.Value = data[2]
		print("Successfully loaded the data for user " .. player.Name)
	elseif not success then
		warn("An error occurred while trying to load the data, see here for more: " .. tostring(result))
	else
		warn("An error occurred, see here for more: " .. tostring(result))
	end

You can do this for as many values you’d like, as long as you repeat the steps above.


8. The End!

Thank you for visiting and reading this tutorial! I hope that it came across as resourceful and beneficial to your career on Roblox.

If you’re too lazy to write or go through all of this, I’ve created a module that contains the singular saving method which you can get here!

If you have any questions or feedback related to this tutorial, feel free to send those in the comments below!

19 Likes

Very nice tutorial, do you think you could possibly explain how to save a player’s inventory whenever they join and leave servers? Since most data saving tutorials always use the leaderstats example.

1 Like

Since tools are objects and not values, we’ll need to convert the objects to values. The way to do this is to synchronize the different values of the tool, and then load it into the player backpack.

So for when you want to save the data you could do something like this:

	local inventoryData = {}
	
	for i, tool: Tool in ipairs(toolsFolder:GetChildren()) do
		table.insert(inventoryData, {
           -- // Save the different values in here
		})
	end

Then you can use UpdateAsync() to save the table values to the player:

repeat
	success, result = pcall(function()
		inventoryStore:UpdateAsync(Key, function()
			return inventoryData
		end)
	end)
	task.wait()
until success

Then you can use :GetAsync() to get the data from our store, and then use the values we saved from the data to create the new tool. As I mentioned in the tutorial, the formula for this is obj.Value = data[n].

repeat
	success, result = pcall(function()
		inventoryData = inventoryDataStore:GetAsync(Key)
	end)
		
	task.wait()
until success or not PlayerService:FindFirstChild(player.Name)
	
if success then
	for i, tool: Tool in ipairs(inventoryData) do
		-- // Make a new tool and load the different values into the tool, then parent it to our folder in the backpack.
	end
4 Likes

:cold_sweat:

Your tutorial also uses methodology that is incredibly oudated and should not be used in any experience that needs to do data saving, especially for heavily data-oriented features. It doesn’t lead to a path of success, it leads to brutal unmaintainability in the long-term.

To point out the most egregious examples:

  • Saving data as an array instead of a dictionary. It’s just not clear to you or your collaborators what data[1] refers to and would rely on you knowing your format at all times as well as that format being entirely unchanging in order. Order should be irrelevant to data keys, plus if you use string keys then it can result in clearer and improved data access. You and your collaborators will thank you more when you can write out data.Money over data[1].

  • Using leaderstats as the source of truth instead of as a visual representation. In short: you lose considerable control and consistency, plus it’s way more trouble than its worth when you want to add more complex or compound data sets such as inventories or want to take the time to really reduce the size of your data when you learn better data saving techniques and go to improve your data architecturing. Today, I could easily reduce the data size of one of my experience’s top players from 0.025 MB (0.63%) to 0.006875 MB (0.17%) due to pure Luau representation.

Both of those are bad ideas for production-level experiences and should only be used at best for experimental and learning springboarding purposes but any well-meaning tutorial that really wants to lead to “success” with DataStores should not be teaching either of those methods but instead the use of dictionaries and how they improve the data saving/tooling experience.

Besides those two, there are also some other scary things that your tutorial suggests doing:

  • Parenting children after parenting an ancestor. The issue with the parent argument of Instance.new also applies to populating an instance, not just its properties.

  • Redefining commonly used variables in functions instead of at the top of the script. Sure, they get cleaned, but why constantly redefine variables that you’ll access frequently anyway? Someone more technically-versed may be able to better expand on this.

  • Using task.wait with no delay while querying a DataStore instead of adding a more appropriate wait time, a retry count and/or exponential backoff. You can exhaust your budget very fast by doing this, especially during slow service response times or full outages.

  • Using UpdateAsync like SetAsync. No, just because UpdateAsync is a conditional set, doesn’t automatically make it safer than SetAsync. You can still encounter data loss with UpdateAsync, and to only use it for that “conditional set” bit is missing out on the entire value of UpdateAsync providing the previous data as an argument to a function.

  • Typechecking is meant for you, the developer, to avoid making mistakes, it doesn’t change the functionality of your code. It would be helpful to clarify that since you’re using typechecking because this isn’t commonly known by most novice developers that I assume this tutorial’s intended audience is.

  • The else condition in the save data function will not be checked. If success is truthy (anything that isn’t false or nil) then the save print will go through, otherwise the first warning (not success checks for falsy) will go through. A boolean can only be true or false, so success can’t be anything else. An additional else would only work if success could be anything other than true or false and that means explicitly checking value equality (first condition becomes if success == false, second condition becomes elseif success == true). For readability purposes, though subjective, it’s preferrable to have the success cases be first in an elseif.

  • Not handling the case of existing players. Code, especially any that yields, can delay when the PlayerAdded connection gets established. When deferred becomes the only usable SignalBehavior, it will no longer be acceptable to just directly connect to PlayerAdded, though you shouldn’t be even doing that ideally even in Immediate mode.

local function playerAdded(player) end

Players.PlayerAdded:Connect(playerAdded)
for _, player in Players:GetPlayers() do
    playerAdded(player)
end

My two cents for success are to use existing DataStore resources. Other developers have already went the mile (for the most part) to create resources that handle all the tough parts about getting the best use out of DataStores that you’d normally have to write yourself, and then you just have to worry about using the correct API to save and load data the way you want. Though of course, learning how to write your own DataStore systems is acceptable, as long as you know how to build upon it to make it better, and that’s where I believe tutorials can teach specific concepts rather than an overgeneralised DataStore tutorial #1000.

14 Likes