Hey!
This tutorial will teach you about regular DataStores (I won’t include OrderedDataStores, but they are pretty similar). DataStore Version 2.0 won’t be included in this tutorial either.
Over the course of this, we will script a DataStore (that will hopefully be safe).
Important: This won’t be as safe as DataStore2 or ProfileService
This won’t be as safe as DataStore2 or ProfileService, as both are heavily tested. So just use them if you need a safe DataStore, I only made this for showing you some ideas that can be applied to DataStores. Bugs in this are possible.
Here a summary:
What are DataStores?
GetAsync
SetAsync
Why is our data not saving??
BindToClose
How do I make my DataStore safer?
Pcalls
Retries
UpdateAsync > SetAsync
Autosaving
BindToClose, continuation
DataStore Corruption
Berezaa’s Method
SessionLocking
We will start with “What are DataStores”.
DataStores are “containers” that you can store data in. They are shared per game, so different places can access and change the same data. You can imagine them as dictionaries.
GetAsync
With GetAsync, you can get the data with a key that you have to pass as the first argument (arguments = the things you pass when calling a function, parameters = the things you receive inside of the function).
So let’s start scripting our DataStore!
Before we use anything, we have to first create it. We use GetDataStore for that.
(btw, I will use ColdCode to screenshot the code I wrote in VSC instead of creating it here, I think that looks better)
Fun fact: you can pass 3 arguments to GetDataStore, the name, the scope and options. Options is just for the upcoming (at the point of writing this tutorial, it’s out now) DataStore 1.1 API though, we will only use the name.
Now that we have created our DataStore, we will define Players as variable and make a function for the PlayerAdded event that we will later connect to it.
Our key will be “Player_” and the userId concatenated to that (you can also use the userId by itself).
Pretty easy right? Just GetAsync on our DataStore and use that data or 0 (in case it’s nil because the player is here for the first time).
You might find it weird how I’m parenting the stuff at the end, it’s because GetAsync can yield. If I parented leaderstats to player right after creating it, how would we know if the player’s cash is 0 because the GetAsync is still trying to get his data or because he is new?
That’s why I parent the leaderstats to the player as last thing, now we can simply use player:WaitForChild(“leaderstats”) and from there on we know that everything is loaded.
Now we just connect that function to the PlayerAdded event and we are done, right?
Not really, we still need to make it save, so also make a save function.
SetAsync
With SetAsync, we set a key to something. I think the easiest way to imagine this is with dictionaries.
We have a dictionary, the keys consist of “Player_” … player.UserId and the values are the cash of the players.
We need 2 arguments for SetAsync: the key and the value.
Kinda like how in a dictionary we would do
So let’s script the function and connect it to PlayerRemoving!
We are using FindFirstChild on the leaderstats because if the player joined and leaved immediately, maybe the GetAsync would still try to get his data - meaning leaderstats isn’t parented yet. Only save something if his leaderstats is loaded (also, we don’t really have time to wait for it, he is leaving).
Alright. That wasn’t hard, was it?
Here is the full code we have:
Why is our data not saving??
Now if we insert that into studio though, you will notice a problem (actually two if you don’t have API Services enabled ): it’s not saving!
Huh? Why is that? Did we do something wrong?
No, we didn’t! Many scripters that are newer to DataStores face this issue.
In fact, I think this is the most common one.
There is just something we didn’t add yet: a BindToClose.
BindToClose
Now, you might wonder: why do I need a BindToClose? We already have a PlayerRemoving event that has a saving function connected to it!
Well, yes, but often, Studio simply shuts down too fast making it impossible for PlayerRemoving to fire.
So what do we do?
→ We will use BindToClose to delay the shutdown.
Now, magically, it works!
So are we done?
→ No, we are not. This DataStore is not safe at all.
How do I make my DataStore safer?
Alright, we are halfway through this tutorial right now. We got the basics down, we just have to improve what we have. This will start becoming a little harder now (don’t worry though, not that hard!).
Have you ever heard of DataStore outages? If you don’t know what those are, they are basically situations, where the DataStores just don’t work: DataStoreService is down.
Now even if there is a DataStore outage or SetAsync, GetAsync, … fail, you want to keep the data-loss as small as possible.
Pcalls
I think the most important thing is to prevent your code from breaking in case SetAsync / GetAsync / UpdateAsync … fails. → You can do this with a pcall!
Pcalls are basically protected calls that catch errors. They return two values: if it was a success and if there were any returns (stuff that you return inside of the pcall or errormessages).
Let’s implement them!
First, for our Get-function.
success becomes a boolean (true/false) and ret becomes our returned value (the cash or in case of a fail the errormessage).
Now, look at the pcall. Doesn’t it look bad how it just calls a function to call another function?
The anonymous function we pass to pcall does nothing but call GetAsync and return that value!
Well, we can fix that! Instead of the anonymous function, we will right away pass GetAsync.
How do we do that? Like this: pcall(dataStore:GetAsync(key))
? No!
First, we have to pass the function → dataStore.GetAsync.
Next, we have to pass self (what we want to call it on) → dataStore.
Now, we just have to pass any arguments that we want to give to it. → key.
I understand if you find this kinda confusing, you just have to know that you first pass the function, then self (the thing you call GetAsync on) and then your arguments.
This will automatically return the result of GetAsync and make ret become that.
Now, how do we continue? We will simply write an if-statement. If it was a success, then make cash become the returned value, if it wasn’t, then print the errormessage.
Let’s do the same for SetAsync!
First the function, then self, then the arguments.
You can also exponentially wait on failure, thanks to Disgusted by You#8532 for telling me about this.
Are we done now?
Not really. Imagine one of these calls fail. The player won’t really care about the errormessage, he will care about his data not loading/saving. So what can we do?
Retries
We can retry if it was not a success. Maybe next time, it will be a success!
I like to use repeat until loops for this, I’ve seen some people use while loops too, that works aswell.
Let’s start with our Get function. Now there is two ways people do this:
Some like to have a variable that counts the retries. If it reached a certain number, then stop retrying, as it’s probably an outage.
or…
just leave out the retries variable and retry until its a success.
If I had to choose I would pick the second option and implement an if-statement that breaks the loop if the player has left. So let’s do that.
We will add a function that yields if there isn’t enough budget.
Thanks to @colbert2677 for suggesting me to use this in one of my posts!
Here is the function, slightly modified as we don’t need two parameters, we will only want one request (originally it had a requestAmount parameter too).
Let’s use that function in our repeat loop!
We pass Enum.DataStoreRequestType.GetAsync as that’s our requestType - should be self-explanatory.
Now, we don’t even need the else statement, it will retry until it’s a success or the player has left.
Now, as you can see, success is underlined!
That can be fixed pretty easily, it’s simply because of scope - the scope of success is inside of the repeat until loop and we are trying to use it outside. Simply define success, ret before.
Now, let’s do the same for SetAsync:
just that here, we won’t stop if the player has left. We will instead continue trying to save - we have all the stuff we need stored in variables. If you are wondering about why I used SetIncrementAsync: it refers to both SetAsync and IncrementAsync.
UpdateAsync > SetAsync
Alright, now our DataStore is already safer than before but we want to make it even safer!
We will switch from SetAsync to UpdateAsync.
UpdateAsync can be pretty confusing at first, especially with that function you have to pass.
UpdateAsync basically takes the key as first parameter - just like SetAsync but a function as second parameter.
Why is that?
Basically, UpdateAsync will give us an “oldData” parameter in the function that we can use.
We just have to return what it should save. → Now it makes more sense, right?
UpdateAsync also ensures that no data is overwritten when another game server updated the key in the short timespan between retrieving the key’s current value and setting the key’s value. → The function will get called again!
So let’s switch from SetAsync to UpdateAsync!
I’m not using UpdateAsync because of the oldData parameter it gives, but because of the reason I stated above.
Autosaving
Now you might be wondering why you need to autosave even though we made this “super safe” PlayerRemoving event. Even though if there is a DataStore outage and the player leaves, it will retry until DataStores are back up again, the server might still shutdown.
In that case, the player’s entire session would be lost VS if we used Autosaving and only a part of the player’s session would be lost.
So let’s implement something like that!
We can right away wrap it in a coroutine - the getRequestBudget function does the waiting if there is no more budget for it for us. When wrapping a function into a coroutine, it returns another function. You can call that and pass the arguments.
Why are you using ipairs?
ipairs is slightly faster than pairs and should be used for arrays.
Why are you using GetPlayers
If you accidentally put a part into the Players service, using GetChildren would try to call save on that part.
A quick note
What if a player joined while we were making these functions? → Because of that, let’s iterate through the Players service and call setUp on every player before connecting our functions with a coroutine.
BindToClose, continuation
Alright, you might be wondering why we need a continuation, isn’t our BindToClose done already?
Well, it looks like this:
This isn’t really how a BindToClose should look like, we just used this so it delays the shutdown in Studio - giving PlayerRemoving time to fire. But what if we aren’t in Studio? → We should save everyone’s data then, Roblox gives us 30 seconds. After those 30 seconds, the game will shut down regardless of our function not being done executing.
I won’t implement any cooldown, even if it adds stuff to the queue.
Now how do we make it wait on PlayerRemoving but not on Shutdown? → We can use an extra parameter!
This might sound confusing but after you’ve seen the code I think it should be clear.
See how I added a new parameter called “dontWait”? If that’s true, then it will not wait.
We will only make it true when the game is shutting down.
Now, we just need a way to check if we are in Studio or not. If we are, then simply wait(1) like we did before, if not, save everyone’s data without waiting. → We can use RunService:IsStudio() for this.
Define RunService at the top and replace our onShutdown function with this:
We are pretty much done with our regular DataStore.
Fun fact: This is the second day of writing this tutorial and I forgot to save the script yesterday and have to rewrite it now. F in the chat for me.
I think if you kept it like this it should be safe already, but what do we do to make it even safer?
→ There is something called DataStore Corruption
It should be pretty rare and as far as I know it shouldn’t occur because of Roblox but because of but to be safe we are still going to handle that.
Now, this will get a little harder: we are going to use Berezaa’s method!
UPDATE: NOW THAT DATASTORE V2.0 IS OUT, THIS IS PROBABLY DEPRECATED, YOU CAN ACHIEVE THE SAME THING USING LISTVERSIONASYNC.
YOU CAN SKIP TO SESSIONLOCKING.
After reading this, you might be a little confused but it’s not that hard, don’t worry.
So basically he is saying that you could have two DataStores for every player: a regular DataStore and an OrderedDataStore. The OrderedDataStore will save the keys of our regular DataStore and our regular DataStore will have versions of data from the player.
First, we will want to make a safeCall function, something that does all these repeat until and pcalls for us, as we will need it for more things now, not only GetAsync and UpdateAsync but GetSortedAsync and the OrderedDataStore version of SetAsync too! If we don’t do that, our code will look like a mess (Don’t-Repeat-Yourself principle).
We will want to make this function very flexible, so that we can use it for basically anything regarding DataStores. Now how do we do that? → We will be using an if-statement to check if an argument exists and the “and” operator.
Let’s do that!
I understand that this might look really confusing to you. I will cover the things I think a newer scripter wouldn’t understand here:
the “…” → Basically, this means “everything followed after that”.
Let me give you an example:
Alright, so when we call doSomething, we pass “hi”, “idk” and “what” into doSomething as arguments.
It receives those in a tuple. What does that mean? Remember how we define success, ret in a pcall?
Pcall basically gives back a tuple!
We can do something similar inside of the function.
Not that hard, right?
Alright, now the next thing that might be hard to understand for a newer scripter is the
I put these two conditions into brackets so it’s easier to look at.
Basically, this is saying that it should stop whe the pcall is a success or when we have given a playerName argument when calling it and it can’t find that playerName string in the Players service (meaning the player has left).
Hm, is there anything else? I don’t think so, so let’s move on.
We can use that function now for basically anything without having to write the repeat loops, so let’s do that for now.
For our setUp function it will look like this:
(damn, what a long line!)
And for our save function like this:
We don’t do “local success, ret =” because we don’t really care about the return, it will try until it has saved successfully.
the (not dontWait and Enum.DataStoreRequestType.UpdateAsync) might confuse you, basically it means that if no dontWait argument was given, it will execute the waitForRequestBudget function with the Enum.DataStoreRequestType.UpdateAsync argument (not nil and object = true and object = object), if the dontWait is true it will become (not true and object = nil and object = nil).
Alright, that’s it! Now we can get into implementing Berezaa’s method!
First, remove the local dataStore = DataStoreService:GetDataStore("Name")
line at the top, every player will have their own dataStore.
Now, inside of the setUp function, we will define two new variables:
Remember, the orderedDataStore holds the keys for our latest versions and our normal DataStore will use that key from the orderedDataStore to get our data. → If our data is corrupted, we can go back one version, if that data is not corrupted, then use that version → better than resetting his stats.
Now, how do we determine if data is corrupted or not? → The GetAsync will error with the errorcode 501 or 504, so we can simply use string.find/match on it.
As far as I know, Roblox can also return false values; for example a string instead of our data, that’s why some people check the type. We are going to do that too.
If 501 or 504 are found in the errormessage, stop the entire function, return nil, we don’t want to retry.
Now, in our setUp function it’s going to be a little more complicated: we get the two DataStores, safeCall GetSortedAsync and get the current page. Then, we check if the current page contains more than 0 elements (if it’s empty it means the player is new), and if so, loop through it, check if the player is still there, if not, stop the function. safeCall GetAsync on our regular DataStore and get the success and ret. If it’s a success (meaning the player didn’t leave after retrying and the data wasn’t corrupted, then set his cash to the ret, parent cash to leaderstats and leaderstats to player and break out of the loop.
Now if the currentPage contains no more than 0 elements, we just set the cash to 0 and parent it and leaderstats.
Note that this would only check the first 100 elements, you could implement a pages:AdvanceToNextPageAsync() to make it so it can use every element, I won’t be doing that as that would make it probably even more confusing.
So basically, it first gets all the pages with 100 elements on each page, then gets the current page (first one), checks if it has more than 0 elements in it and then iterates through it, checking if the player is still there, setting the dataStoreKey to dataStoreKey.value (GetSortedAsync gives stuff back in dictionaries that have a key and a value, we are only interested in the value (it will be our dataStoreKey).
safeCall GetAsync on our dataStore with that key and if it’s a success, then set the cashValue to the returned value, if it’s not then it will simply continue iterating; it can only not be a success if the player has left or if it’s corrupted. At the beginning we have an if statement that stops the function if the player has left, so it will only try with an older version if it was corrupted.
Now, let’s get to our save function. This one will be shorter.
The only thing we have to do is get the latest key and add 1 to that, save that into our OrderedDataStore and use it as key for our regular DataStore to save the player’s new data.
Alright. Let’s test this now. Play → set our cash to 500 on the server, leave, play again and boom it works!
Now let’s test the corruption handling. I set my cashValue to 80, left, then 90, left and added this to our safecall to simulate a corruption:
It worked perfectly for me, it has gone back to 80.
SessionLocking
Alright, now this is even more important than Corruption Handling in my opinion, as Corruption occurs very rarely as far as I know.
Imagine following scenario: your game has a trading system. Player1 trades with his friend, Player2, and gives him his diamond sword. Now, Player1 quickly leaves. Our save function takes a bit longer than usualy to save (UpdateAsync fails a couple of times for example). Now, in that time Player1 rejoins and gets his old diamond sword that he just traded away back. Why? Because our DataStore still didn’t save his new data, our GetAsync got his old one. Now, Player1 and Player2 both have diamond swords → they discovered a method to dupe. Now even if our DataStore finally saved, when Player1 leaves, it will save his old diamond sword that he got when rejoining.
So what can we do to stop that?
→ We could set a value in their data to true when they join and to false when saving. In our PlayerAdded event, we can check if that value is true and if so, kick them or retry until it’s false, the DataStore still didn’t save.
@ArtFoundation did a good job at explaining this here: Session locking explained (Datastore)
So let’s try making something like that, to make this less complicated, I won’t be using our code that handles corruptions (I don’t even know if you could implement SessionLocking into there, it saves a new version everytimes, it would be tricky probably), but our old one that simply retries. I also won’t be using our safeCall function. I will remove the requestType parameter from waitForRequestBudget as we will only use Enum.DataStoreRequestType.UpdateAsync.
This will be easier than Berezaa’s method. Let’s start with our Get function:
Basically we are checking if the oldData is SessionLocked, and if so, set the shouldWait to true → it will wait 5 seconds and retry. If not, we will set it to true, put our oldData into our data variable (we replaced an additional GetAsync like this) → save the data we set the SessionLock to true.
Now, we just check if it was a success (if not, that means the player has left) and the data is there, set the cash to the data-table’s cash and parent everything.
For our Save function we have two options:
it’s autosaving, which means we should keep the SessionLock true,
the player is leaving, which means we should make the SessionLock false.
So I added an extra parameter called “dontLeave”. This will be set to nil if the player is leaving and to true when we autosave.
shouldLock basically says “if dontLeave is true then make it true, else make it false”
Because of that, we have to change our autosave and leave functions so they pass the correct arguments:
And we should be done.
Now, the only problem is that if the game somehow shuts down and not everyone’s SessionLock could be set to nil, the player is going to be banned.
In order to fix that, we can use an os.time() value as SessionLock instead and declare a session as dead if the current time minus that time is more than 30 minutes.
We will change our setUp function to:
Basically, we set ret to “Wait” if the session is not dead yet, and if it’s dead, we just take over the session like if there was no SessionLock at all.
In our save function, we only have to change one line:
to
So at this point we are pretty much done with this tutorial, I’m going to give you the code for the Session-Locking DataStore:
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local dataStore = DataStoreService:GetDataStore("Players")
local default = {
SessionLock = false,
Cash = 0
}
local updateAsync = Enum.DataStoreRequestType.UpdateAsync
local function waitForRequestBudget()
local currentBudget = DataStoreService:GetRequestBudgetForRequestType(updateAsync)
while currentBudget < 1 do
currentBudget = DataStoreService:GetRequestBudgetForRequestType(updateAsync)
wait(5)
end
end
local function setUp(player)
local name = player.Name
local userId = player.UserId
local key = "Player_" .. userId
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
local cash = Instance.new("IntValue")
cash.Name = "Cash"
local success, data, shouldWait
repeat
waitForRequestBudget()
success = pcall(dataStore.UpdateAsync, dataStore, key, function(oldData)
oldData = oldData or default
if oldData.SessionLock then
--He is still sessionlocked, so just wait
if os.time() - oldData.SessionLock < 1800 then
--session is alive
shouldWait = true
else
--session is dead, take over
oldData.SessionLock = os.time()
data = oldData
return data
end
else
oldData.SessionLock = os.time()
data = oldData
return data
end
end)
if shouldWait then
task.wait(5)
shouldWait = false
end
until (success and data) or not Players:FindFirstChild(name)
if success and data then
cash.Value = data.Cash
cash.Parent = leaderstats
leaderstats.Parent = player
end
end
local function save(player, dontLeave, dontWait)
local userId = player.UserId
local key = "Player_" .. userId
local leaderstats = player:FindFirstChild("leaderstats")
if leaderstats then
local cashValue = leaderstats.Cash.Value
local success
repeat
if not dontWait then
waitForRequestBudget()
end
success = pcall(dataStore.UpdateAsync, dataStore, key, function()
return {
SessionLock = dontLeave and os.time() or nil,
Cash = cashValue
}
end)
until success
end
end
local function onShutdown()
if RunService:IsStudio() then
task.wait(2)
else
local finished = Instance.new("BindableEvent")
local allPlayers = Players:GetPlayers()
local leftPlayers = #allPlayers
for _,player in ipairs(allPlayers) do
coroutine.wrap(function()
save(player, nil, true)
leftPlayers -= 1
if leftPlayers == 0 then
finished:Fire()
end
end)()
end
finished.Event:Wait()
end
end
for _,player in ipairs(Players:GetPlayers()) do
coroutine.wrap(setUp)(player)
end
Players.PlayerAdded:Connect(setUp)
Players.PlayerRemoving:Connect(save)
game:BindToClose(onShutdown)
while true do
wait(60)
for _,player in ipairs(Players:GetPlayers()) do
coroutine.wrap(save)(player, true)
end
end
and here for the Berezaa’s method one:
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local function waitForRequestBudget(requestType)
local currentBudget = DataStoreService:GetRequestBudgetForRequestType(requestType)
while currentBudget < 1 do
currentBudget = DataStoreService:GetRequestBudgetForRequestType(requestType)
wait(5)
end
end
local function safeCall(playerName, func, self, requestType, ...)
local success, ret
repeat
if requestType then
waitForRequestBudget(requestType)
end
success, ret = pcall(func, self, ...)
if not success then
print("Error: " .. ret)
if string.find(ret, "501") or string.find(ret, "504") then
return
end
end
until (success) or (playerName and not Players:FindFirstChild(playerName))
return success, ret
end
local function setUp(player)
local name = player.Name
local userId = player.UserId
local key = "Player_" .. userId
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
local cash = Instance.new("IntValue")
cash.Name = "Cash"
local dataStore = DataStoreService:GetDataStore(key)
local orderedDataStore = DataStoreService:GetOrderedDataStore(key)
local _, pages = safeCall(name, orderedDataStore.GetSortedAsync, orderedDataStore, Enum.DataStoreRequestType.GetSortedAsync, false, 100)
local currentPage = pages:GetCurrentPage()
if #currentPage > 0 then
for _, dataStoreKey in ipairs(currentPage) do
if not Players:FindFirstChild(name) then return end
dataStoreKey = dataStoreKey.value
local success, ret = safeCall(name, dataStore.GetAsync, dataStore, Enum.DataStoreRequestType.GetAsync, dataStoreKey)
if success then
cash.Value = ret
cash.Parent = leaderstats
leaderstats.Parent = player
break
end
end
else
cash.Value = 0
cash.Parent = leaderstats
leaderstats.Parent = player
end
end
local function save(player, dontWait)
local userId = player.UserId
local key = "Player_" .. userId
local leaderstats = player:FindFirstChild("leaderstats")
if leaderstats then
local cashValue = leaderstats.Cash.Value
local dataStore = DataStoreService:GetDataStore(key)
local orderedDataStore = DataStoreService:GetOrderedDataStore(key)
local _, pages = safeCall(nil, orderedDataStore.GetSortedAsync, orderedDataStore, Enum.DataStoreRequestType.GetSortedAsync, false, 1)
local latest = pages:GetCurrentPage()[1] or 0
local should = (type(latest) == "table" and latest.value or 0) + 1
safeCall(nil, orderedDataStore.UpdateAsync, orderedDataStore, (not dontWait and Enum.DataStoreRequestType.UpdateAsync), should, function()
return should
end)
safeCall(nil, dataStore.UpdateAsync, dataStore, (not dontWait and Enum.DataStoreRequestType.UpdateAsync), should, function()
return cashValue
end)
end
end
local function onShutdown()
if RunService:IsStudio() then
task.wait(2)
else
local finished = Instance.new("BindableEvent")
local allPlayers = Players:GetPlayers()
local leftPlayers = #allPlayers
for _,player in ipairs(allPlayers) do
coroutine.wrap(function()
save(player, true)
leftPlayers -= 1
if leftPlayers == 0 then
finished:Fire()
end
end)()
end
finished.Event:Wait()
end
end
for _, player in ipairs(Players:GetPlayers()) do
coroutine.wrap(setUp)(player)
end
Players.PlayerAdded:Connect(setUp)
Players.PlayerRemoving:Connect(save)
game:BindToClose(onShutdown)
while true do
wait(60)
for _, player in ipairs(Players:GetPlayers()) do
coroutine.wrap(save)(player)
end
end
Update: Now that the task library is out, use task.wait() instead of wait().
I hope you liked it!
It’s possible that some things in this tutorial are wrong or there are bugs, if you find something, please tell me.
If you see anything wrong in this / anything missing, just reply to this tutorial.
I, myself, most of the time just use ProfileService.
ProfileService and DataStore2 are both heavily tested so probably a lot safer than this.