Stop using SetAsync() to save player data

Update Notice 2022

If you want to setup reliable datastores quickly and without having to worry about all the quirks which come with the datastore API, definitely check out ProfileService.

This thread is reasonably outdated whereas a resource like ProfileService is actively maintained by @loleris. It covers scenarios you might not typically account for such as preventing the duplication of items when trading with session-locking.

Quick Disclaimer 2021

This post is a few years old now and my attitudes and practises have slightly changed - I aim to create a re-write of this thread sometime in the future. There will likely be many updates to DataStores by Roblox itself since the time of writing this too. Nevertheless this is still a useful tutorial and I’d highly recommend reading the responses from people such as @colbert2677 and @filiptibell who arguably provide equally if not more important tips and discussion.


Background

For the majority of games, there comes a point where data about an individual needs to be saved (e.g. level, items, cash, etc). To achieve this, we use DataStores.

Now ask any developer, what did you find most challenging about creating a game, and you’ll find a good majority respond “DataStores”. There are various reasons for this: they require a good understanding of lua, take significant time to initially learn about and setup, and are difficult to test compared to typical features. There is one issue however which dominates all; one with real-world consequences and potentially disastrous results: the loss of player data.


Consequences of data-loss

The loss of player data can have severe repercussions, both for the user and developer:

  • Significant time spent trying to restore data and refund players.
  • Loss of players/reputation. This is fairly self explanatory; if a player loses their data there’s a good chance of them quitting for good and leaving a negative review in the process.
  • Ethical consequences. To some, a player might just be text and a character on the screen, however you have to realise each individual is a real human. That 1000 R$ might mean nothing to you, but for the player was 10-weeks of saving up pocket money to afford it. Some players may have also invested huge amounts of time into your game. The loss of many-months of progress may cause them personal distress.

What can be done to limit this?

Obviously no developer desires this, however speaking with others this is something almost everyone, including myself, experience at one point or another. No need to fret though, there are measures you can take to prevent or at least minimise data-loss and its effects:

  • Use a separate datastore to specifically handle developer product purchases. If worst comes to worst, this will make refunding players enormously quicker and easier to do. It doesn’t take long to implement and could really pay-off in the long run.
  • Consider setting up your datastores using DataStore2. In simple, it provides an easy way for you to setup datastores without having to worry about the technical side to saving data. It utilises the DataStore and OrderedDataStore services so that data can’t be overwritten and gives you the ability to retrieve previous data versions. You can find out more here.
  • Verifying data before writing it to the datastore. The data retrieved using GetAsync() is not always accurate or correct. For example, if GetAsync() fails then the players data may be set to false or nil. If this value is written to the datastore, the player’s data will be completely overwritten. GetAsync() can also retrieve incorrect previous data if a player hops between servers too quickly. To prevent this, we use UpdateAsync().

Writing data

There are four methods of writing data:

  1. SetAsync
  2. UpdateAsync
  3. IncrementAsync
  4. RemoveAsync

Now there’s a good chance you’ve heard of SetAsync(), but what about UpdateAsync()? If you haven’t, shame on you as the main page even highlights it in yellow for you:

As the warning explains, SetAsync simply writes a new value to a key without any consideration for the previous value, whereas UpdateAsync retrieves the value of a key from the datastore and updates it with a new value - this gives you the ability to compare the data about to be saved with the previous and act accordingly.


Writing data correctly

So obviously we should all be using UpdateAsync() to save player data, right? Yes! However there is still a handful of people using SetAsync() when they really shouldn’t. Even an official tutorial by Roblox on Saving Player Data uses this incorrectly.

Here’s an example of UpdateAsync() and incremental-values used to effectively save data:

You vs the guy she says not to worry about

Source code
if playersData then
	datastore:UpdateAsync(key, function(oldValue)
		local previousData = oldValue or {DataId = 0}
		if playersData.DataId == previousData.DataId then
			playersData.DataId = playersData.DataId + 1
			return playersData
		else
			return nil
		end
	end)
end

Note: nil is returned for scenarios where we don’t want the data to be updated. It effectively cancels the save. You should also wrap your functions in pcalls when retrieving and setting data.


Epilogue

I’m guilty myself of using SetAsync(). Back in 2017 I had a game which blew up to front page. This was an awesome experience but also created a datastore nightmare at the time too. We were having reports of data-loss at least 20 times a day which, as you can imagine, created a huge headache; the next week of my time was completely devoted to rewriting the datastore and refunding as many players as possible.

In-a-way, this was a blessing in disguise as it forced me to develop good practices when saving data. Now-a-days, I will always use UpdateAsync() with an incremental-value when saving important data. More recently, I released a multi-place game where players are constantly teleporting between servers, yet to this day have had 0 reports of data-loss.


Summary

Here’s a brief overview by @colbert2677:

  • UpdateAsync is the canonical way to update data. Get should be used to retrieve data and set should be used when you need to force data. This is also an official recommendation as stated on the Developer Hub.
  • UpdateAsync respects previous data in the DataStore. If you don’t use this, you have to tread fairly carefully when doing a Get-Set method.
  • UpdateAsync respects conflicting calls. Data won’t force itself as the new value or try to overstep other calls that are also attempting to write to the DataStore.
  • Good practice

Thanks for reading. Please get in touch if you have any question.

If you’re interested in setting up your own datastore, feel free to check out my other brief tutorial here: How would you use ROBLOX's datastorage API? - #3 by ForeverHD

353 Likes
Causes of player data loss
How «safe» are data stores?
How should i handle DataStores failure?
How could I improve the neatness and efficiency of my DataStore code?
[ERROR] Leaderboard storing "Shirt Sales"
Datastore Code Review
Help with :UpdateAsync()
How do you use UpdateAsync() correctly?
How can I improve my basic datastore?
Help With Leaderstats
Thoughts on this this auto-sword saving datastore?
Datastore Tutorial for Beginners
Data not Saving
DataStore Saving Problem
Datastore script not working
Datastore service throttled
Datastore set taking forever to save
Is this a safe DataStore?
Inventory Save Bug
How to make this coin gui script save with data store
Roblox Datastore
Can someone help me fix this mixup with datastore?
:SetAsync() v.s. :UpdateAsync() which one should I use?!
How to make a Strong DataStore
Using UpdateAsync to save tables isn't working
Data store not saving?
How can I improve this datastore?
DataStore Scheme
DataStore Problem/Data Loss
Why does my datastore script not work?
Datastore limitations and best practices queries
Confused on using UpdateAsync
Help saving data script
How would I go about scripting a quest system
Anti data store corruption
How to properly utilize UpdateAsync
How to properly utilize UpdateAsync
First time Data Store
Any ways to improve my datastore?
Quick Data store question
Separate DataStores & Multiple Values - Improvement Needed
Datastore request was added to queue. If request fills, further request will be dropped
Confused with UpdateAsync
How do I save player interaction
Why isnt my datastore saving?
Data Save Script Won't Work (I figured it out)
Dataloss with my custom data module
Is there any other way to secure this DataStore script?
Why Does The Datastore Fail?
Database won't save... [Datastore2 Usage, Please Help]
Urgent data loss issue
How to use Datastore-Service correctly and fix dataloss
Preventing item duplication via trading
How do I save coins and items using datastores?
Player Experiencing Data Loss in my TD Game
Sometimes the IPA request is failing for a player that joins the game. Can someone tell how to fix this?
Data Store script not working
Datastore Leaderstats
Leaderstsat does not show up
Datastore retrieving a nil value?
What better ways are there to storing user data in a DataStore?
Data Saving Issues
Sometimes players lose some of their data
Sometimes players lose some of their data
How «safe» are data stores?
Why this script don't save table?
Help needed with databases
Leaderstats not saving
Tool / Inventory Saving Script Not Working
Can you save tables?
Having issues with datastore
Trouble with leaderstats saving
DataStore | Problems With Loading and Saving
DataStore not working
Value that is clearly in table, keeps returning false?
Pcall(function()) not returning it's success or error?
Storing a table into datastore doesnt work?
How do i create a saving system for parts using DataStores?
Data handling, players losing data "often"
Is it possible to create a Table and save/Load it?
DataPro [Version 1]
Data Not Saving 2
Datastores not working properly
How can i fix my datastore script?
Datastore not working in studio
How can I improve my DataStore?
I want to save tables in Data Store!
Help With Saving Table to Datastore
SetAsync Not working (saving player data)

Thanks for the tutorial! Looks like I need to go back and rewrite some code.

17 Likes

A really helpful tutorial. I will use this in a future for my big games :slight_smile: :wink:

4 Likes

Well, you are talking bad about SetAsync a bit too much, when it comes to complex data management UpdateAsync isn’t suitable for it however I would agree that people should use UpdateAsync over SetAsync in a scenario that your Code is simple and not complex.

My case however I write my own UpdateAsync combined with the same functionality and concept from DataStore2 (Originally berezaa’s method of saving data), Using DataStore and OrderedDataStore to save Data with a Time Line Record.

You also forgot to mention that DataStore has it’s limits, using pcall and some other functionality but I do really like your Threads they are fun to read, give insight and helpful. :+1:

10 Likes

Awesome tutorial, I will definitely use this in the future! Thanks.

2 Likes

Sure, if you’re working on a small project or using non-essential data that’s fine - UpdateAsync does take more time and consideration to implement. However, if you are working with data where players are purchasing goods (e.g. Cash) for real-life currency then it is vital you are doing your best to ensure their data is secure as it has real-life consequences.

Could you provide an example of this? I’m using UpdateAsync() for fairly complex systems in HD Admin which sometimes works with 20k+ concurrent players at the weekends, yet haven’t experienced any technical problems or limitations.

I haven’t included it in this tutorial as I wanted to focus more on the writing aspects than error handling. You are correct though, pcalls and error handling are vital components when managing datastores!

9 Likes

Couldn’t agree more pal!


I made my own Custom DataStore2 with the same functionality however you can’t achieve the same results using UpdateAsync (You could but it might be a longer process)

Although I’m not saying that it isn’t useful because I am using UpdateAsync for Checking a Game Place Version, if there is a new version auto Soft-Shutdown, I might create a system that doesn’t require rejoining but rather update the game internally by itself.

11 Likes

ABSOLUTELY. Instant thumbs up the moment I saw the thread’s title. UpdateAsync is overwhelmingly underused and it shouldn’t be. I don’t know why I never wrote about it but I probably wouldn’t be able to explain it the same way.

There should be very few circumstances in which you actually need to use SetAsync. All other data should be saved with UpdateAsync. The methods are named the way they are for a reason - update respects previous data and conflicting write requests, set does not. That being said, I also now include transform functions for ANY library I create that has to do with handling data as well.

Foregoing the use of UpdateAsync to use SetAsync in all cases of saving is a fairly dangerous practice that needs to be let go by many developers. This is hurtful to both user experience once the problems start rolling in and to the developers for issues related to their projects, as well as to the knowledge they hold of the DataStore API. This could also carry over to other developers, such as when answering questions on the DevForum.

It also cannot be expressed enough that DataStore calls should be wrapped in a pcall! This is so utterly important in terms of handling errors in DataStores yourself and ensuring that issues with DataStore methods do not cause further damage. Anything that is internally (even externally) a web call should be wrapped in a pcall. Write the responses to the console for your own viewing and do something for the user to notify them that their data failed to load. I personally block save requests or merge session data to saved data in case of error.

Cheers for writing this. :tada:

24 Likes

I thought I had to make changes in my code, but apparently my friend already made them for me. (SaveAsync is UpdateAsync, I just haven’t updated it).

All updated!

9 Likes

Oh boy time to change all my datastore code. Thanks for the information though!

1 Like

All current data store code I’ve made previously have been adapted to UpdateAsync().

Thanks for the information, have a nice day.

4 Likes

I fail to see how using UpdateAsync() (at least the code you provided) prevents data loss. Wouldn’t oldValue be the exact same thing returned by a GetAsync() call? Or am I missing something here? I also fail to see what the existence of DataId does.

(Assume a 100% success rate with GetAsync() because it is very easy to not save if the call fails.)

7 Likes

I have to agree with @Rocky28447 on this one. I do not see how UpdateAsync prevents data loss at all when it comes to saving player data.

I believe that most people experiencing data loss with SetAsync have horrible coding structure and logic, which of course will result in players losing data. I’d really like to see what you changed your code from and to to see the difference.

One way players can lose data is if developers wanted to update players’s data but ended up having a mistake in their code, which resulted in it replacing the actual saved data.

The only benefit (that I can see and make it worth putting up here) UpdateAsync has is in case you are updating a key with some new data often, where it will retrieve the latest updated data and return it so you can modify it in the way you like.

If anyone has valid reasons as to why UpdateAsync is better to use than SetAsync, then please inform me as it would be in the interest of everyone to know this.

Also:

Another possible reason why developers say DataStores is difficult, is due to Roblox’s horrible data stores. You never know when it may fail or not, and you don’t know if the data you just retrieved was the player’s actual data, or, if what you just tried to save, actually saved.

18 Likes

Sure, UpdateAsync() used by itself would act the same as SetAsync(), however when utilised fully provides two huge benefits:

  1. It doesn’t overwrite previous data if nil is returned - If your function somehow messes up then it won’t wipe the player’s data.
  2. It considers the old value before making changes . This enables you to implement a value which is changed each time data is saved (the incremental value). As @colbert2677 mentioned, UpdateAsync respects previous data and conflicting write requests.

When GetAsync() fails and returns a false/nil value, then you are correct, the error can be handled appropriately using either method. However, when GetAsync() fails and returns an old, undesired version of data, UpdateAsync can check for this with the incremental value. In most cases of saving data, the DataId will match the previous DataId. This means the two sets of data are consecutive, therefore can be saved without worry. However, as @1TheCutestDog mentioned:

You can check for this though with UpdateAsync() as the two DataId values will not match, allowing you to cancel the save and respond accordingly.

7 Likes

Helps to use the whole context and not just a select part of it to make a point.

oldValue is indeed what would be passed from a GetAsync call, however the retrieval and transformation of that value is handled all with a single request from the budget. It’s the value, not the call. DataId is an example in this code, it’s nothing relevant.

On top of the fact that UpdateAsync allows you to specify a transformation function and reject save attempts, you miss the merit when you use SetAsync

  • UpdateAsync is the canonical way to update data. Get should be used to retrieve data and set should be used when you need to force data. This is also an official recommendation as stated on the Developer Hub.

  • UpdateAsync respects previous data in the DataStore. If you don’t use this, you have to tread fairly carefully when doing a Get-Set method.

  • UpdateAsync respects conflicting calls. Data won’t force itself as the new value or try to overstep other calls that are also attempting to write to the DataStore.

  • Good practice. :triumph:

12 Likes

Great tutorial! I’ll definitely be checking how I wrote the DataStore scripts in my old projects.

3 Likes

Thanks for the additional points! I’ve added these to the main post.

1 Like

Your points are mostly valid, however, for saving player data, there is no real reason not to use SetAsync.

  • UpdateAsync is used for keys that you are updating frequently and that multiple servers could write to at the same time, and for this case, you use UpdateAsync, not SetAsync as you could be overwriting or ignoring other values the other servers may have written to it (that are pending).

  • For player data: you save the player’s data only when the player leaves or when an important action is happening. For this case, you use SetAsync or UpdateAsync. Which you choose is up to you. Just know that it doesn’t really matter.

I am not trying to say that UpdateAsync is horrible or that SetAsync is horrible, I was just trying to prove my points related to saving player data.

5 Likes

You should also read the post. It’s not necessarily about preventing data loss because there’s no surefire way to do so when we have zero control over the servers that the data is uploaded to. Even then, data loss is not something that can be permanently prevented but it can be heavily mitigated to give that comforting feel that your servers are fail proof.

UpdateAsync has more usefulness than often thought. No one’s going to hound you for not using it but people are going to pitch that recommendation out for you. In some cases, if you’re working on a project, this may prompt a rewrite depending on the decision of a project head.

Not necessarily. Both methods are equally at risk for data loss or incorrect updates if you have “bad coding structure”. The difference is that SetAsync forces data. It doesn’t respect conflicting calls, it doesn’t transform data, it doesn’t validate data and you can’t cancel it once you initiate it. Obviously you can set up your own code but you can’t get all the behaviours from UpdateAsync in an API incorporating SetAsync (such as overriding calls).

There isn’t much to see in the first place. I could use general examples, avoiding any custom developed APIs.

SetAsync:

local Data = {} -- Data

DataStore:SetAsync("Key", Data) -- Save

UpdateAsync:

local Data = {}
local NO_SAVE = false

DataStore:UpdateAsync("Key", function(oldData)
    if NO_SAVE then
        return nil -- Cancel the save
    end

    if oldData then -- Something exists
        -- Merge contents and return table
    else
        return Data -- If there's no old data, use current set
    end

    return nil -- If no code above this returns anything, cancel save
end)

This point ignores a lot of the merit to UpdateAsync raised in this thread. Reasons have been provided as to why UpdateAsync is powerful and useful. I don’t feel like parroting those points, but this is not the only benefit.

This whole thread and the discussions in the responses should be reason enough? No method is better than another, though one method is more appropriate than another depending on the presented scenario. I’m not sure what point you’re trying to make here.

That depends on what kind of difficulty you’re referencing. It can be difficult to work with DataStore failures in mind. Difficulty using the actual API is a different story of understanding how to apply the service and use it’s methods to achieve your goal.

You do, if you wrap it in a pcall. The only time you don’t is if the call is successful and doesn’t return an intended value (e.g. the incident report wherein DataStores were caching and returning nil instead of actual data).

You know this as well. UpdateAsync is a callback, it returns the data that is entered to the key in the DataStore after the call is completed (whether it saves that data or not). This is their actual data. You can also confirm, with this return, whether it saved or not.

13 Likes

No one is saying don’t use SetAsync, but it is discouraged for using it in a majority of cases. Realistically you should only be using SetAsync if you need to force a certain type of data to stick without respect to any other calls. Player data is a very fragile and significant thing, so mitigating any chance of failure or impropriety is valuable.

Again, that also depends on the scenario at play. For a player leaving, I would change it depending on what is going on during that leave. You can’t differentiate between a teleport and a regular leave; only that the player is gone.

For example, a game I’m developing involves teleporting between places fairly often (but not extremely frequent). I would rather use UpdateAsync so that a player can enter another server with accurate data (and so I can pull off some neat tricks, such as sending the return of UpdateAsync with the player to the new place via MessagingService or server-sided teleport data).

Old relevant discussion:

There may be other threads discussing the usage of these two. I just remember this one off of the top of my head.

6 Likes