How to properly save player data in data stores upon server close

OH NO.

Your script that saves data doesn’t seem to work. If that’s you, you’re in the right place. But why is this?

What’s going on?

First off, some data loading and saving scripts look kind of like this

-- in this very specific example it's saving cash

local Players, DataStoreService = game:GetService("Players"), game:GetService("DataStoreService")
local cash_data_store = DataStoreService:GetDataStore("CashDataStore")

local function load(player: Player)
    -- ...
end

local function save(player: Player)
    -- ...
end

Players.PlayerAdded:Connect(function(player: Player)
   load(player)
end)

Players.PlayerRemoving:Connect(function(player: Player)
    save(player)
end)

It’s pretty simple process. They join, you load their data, they leave, save that data. You might be thinking that this alone is fine. Well that is where you’re wrong.

This can fail in certain circumstances. When a server is in the process of shutting down, its priority is shutting down and stops doing anything else.

The 3 ways that come to mind are these (do give me more if there are):

  • The last player leaves the server
  • A developer manually shut it down via the game page
  • A network error is encountered and the server therefore is forced to shut down

In these cases, the PlayerRemoving event listener is never called or doesn’t finish.

What is the solution?

It’s a pretty simple solution. But there’s a catch. I’ll get to that soon.

In addition to saving upon a player’s removal, also save when the server shuts down. This can be done via the DataModel:BindToClose method, which takes a function as an argument, and is called when the server shuts down. Multiple functions can be bound, and they’ll all be called in a separate thread when the server shuts down.

game:BindToClose(function()
    for _, player in ipairs(Players:GetPlayers()) do
        save(player)
    end
end)

And there it is. In addition to data saving when the player leaves, it also saves the remaining players’ data when the server shuts down.

Remember that I said there’s a catch? Here’s the catch:

Roblox only allows 30 seconds for all the bound functions to execute. NOT 30 FOR EACH ONE. But 30 seconds for all of them execute. When those 30 seconds are over, the server shuts down regardless of if they’re done.

This probably wouldn’t be an issue if data store requests didn’t take indeterminate time to complete. Yes, since data store requests are web requests, they can take a long time depending on the responsiveness speed of the Roblox servers and other factors. This can get even more troublesome if your game can have many players. In my experience a web request took about 1/2 to 1 second to complete. Imagine having 30-player servers. And Roblox wants 700+ player servers apparently. Unfortunately I don’t believe we can accelerate the speed that Roblox picks up our requests.

The issue with this code is it saves data asynchronously rather than synchronously.

What’s going on in that code is, it makes a web request, then waits for its completion. It then makes another one, resulting in a waste of time. There’s that possibility it can go over that 30 second cap, which means the remaining players’ data wouldn’t be saved.

This time should be used to save other players’ data as well. This probably won’t make sense right now but I’ll explain soon.

If you’re unfamiliar with multithreading, only a single thread (“coroutine”) can execute at a given time. They aren’t truly running in parallel, but it seems like they do by switching flawlessly between each other when you yield. Coroutines can be “resumed” by in another coroutine. A coroutine can also “yield” (pause) to pause through its code and give control back to whatever resumed it.

And that’s all you really need. When you use a function that is labelled with Async (e.g. GlobalDataStore:SetAsync), it takes indeterminate time to finish, and when you call them, they yield the current thread until the request is completed, and selects a thread that Roblox manages to resume

So what happens when we apply this knowledge into our code?

game:BindToClose(function()
    for _, player in ipairs(Players:GetPlayers()) do
        coroutine.wrap(save)(player)
    end
end)

And just like that, the data should properly be saved.

I’ve also seen some scripts where they wait() (or yield in some form) in the function, presumably because the writer thinks it buys them more time. That doesn’t buy you any more time.

Or even

game:BindToClose(function()
    wait(n)
end)

Which I guess would work, as it stalls the close, but I don’t recommend just yielding. More meaningful work should be done.


I hope you liked this tutorial and found it helpful. This is my first time making a tutorial on here and surely there’s room for improvement. Any feedback is appreciated.

187 Likes
DataStore SetAsync not working
How to make it so data saves when a player leaves and they were the only one in it
DataStore not Saving
Datastore Issue
Datastore difficulties
Best way to use DataStore?
How can I avoid data loss (without using DataStorev2 or ProfileService) in the DataStore?
Does PlayerRemoving() and BindToClose() fire after the last player leaves?
DataStores Not Working
Datastore help please!
Is there a better way to constantly check values of a player?
DataStore isn't Saving
You need to use BindToClose when using DataStoreService
Catch-All PlayerAdded / PlayerRemoving Functions Module
DataStore Saving
DataStore request was added to queue. If request queue fills, further requests will be dropped. Try sending fewer requests
Leaderstats and datastore
Data store doesn't save
Data store doesn't save
Data store doesn't save
Programmer, Translator (English -> Spanish) Portfolio
Help with Global Datastore
Does the PlayerRemoving event not fire when disonnected?
Few Questions on dataStore
Help with datastores
Help with DataStore not saving/working (PlayerRemoving/game:BindToClose)
Local data tables gives nil?
Will Players.PlayerRemoving consistently fire?
Data store isn't saving
What are the DataStore Basics
Datastore wont save 2 values
[HELP] How can I fix this script
I feel like this script was too easy to script
Help with (coroutine.wrap)
How to ensure that an arbitrary callback runs for players exactly once when they leave or the server shuts down
[R$10,000 Bounty] Random Data Loss
Data stores just not working
Need help with DataStore
Trouble with leaderstats saving
Datastore difficulties
Storing a table into datastore doesnt work?
Why this is not working (datasave)
Datastores not working properly
Datastore is not saving a string value when the player leaves the game
Something wrong with DataStore
DataSave is not working
Save Data not work, how to fix it?
DataStore not saving when leaving
How would I add BindToClose into my datastore?
Help with Saving Data on Shutdown
Async in methods

Data saving is a very prominent topic and use case for games all across Roblox, so it’s important to address best practices when it comes to saving data. This ensures that both developers and players have the peace of mind that they will be able to come back to little issues regarding data.

Due to the importance of data saving, it’s imperative that any shared information is also accurate and teaches either the depths of a certain topic or the baby steps towards improving data. Thank you for sharing your insight towards this kind of a topic.

During my read, I noticed a few things that I wanted to provide feedback on. If you disagree at any point, please do point it out. I’m no expert in these fields but I know enough to say something.


PlayerRemoving is fired for the last player in a server. The instance is kept alive for a short amount of time before closing. Consequently, you are also able to save data in single player games where the maximum player count per instance is 1.


game:BindToClose(function()
    for _, client in ipairs(Players:GetPlayers()) do
        save(client)
    end
end)

When an instance closes, its players are destroyed. That means all the descendants of a player are removed as well. In this case (as well as in most cases), it’s typically better not to keep data that you intend to save under the player object due to this and to instead store it elsewhere, such as ServerStorage. This is also better for control purposes overall.

There isn’t an issue with saving data that is stored under the player object, however that presents the potential for edge cases wherein the intended data container gets removed before you have a chance to interact with it, thus lost data. That’s why keeping it out is better.

In a typical data structure, what I do is keep a data folder under ServerStorage. Each descendant folder is the UserId of an active player in the server and the contents of those folders are their full data sets with further organisation as I see fit. Then, for my BindToClose function, I iterate through that instead of players. Everything from there is as you’d expect; the name of the folder, the UserId, is passed as the key, while the value is an array constructed from that folder.


Latency doesn’t affect the speed in which DataStore requests take to complete. Roblox is sending an API request to its own endpoints, something HttpService isn’t allowed to do for security and safety purposes. Keep in mind that no request yields, so the time it takes to complete the request is actually determinant on the responsiveness of the endpoint.


Hardly an issue so long as you aren’t yielding. All my saving operations are instantaneous, I never have a reason to use a wait in them. If I need to yield, I tell myself that it’s wrong and change my structure or code so that I don’t need to.

It’s circumstantial but typically you should never be yielding within a saving operation. The DataStore write method you use, if it yields, is of no concern. Typically doesn’t take a noticeable amount of time to complete any request.

You make a good case for pseudothreading below, but I figure I should point this out anyway.


This. Thank you for being the one to break it to people. It’s absolutely pointless to include the statement wait(30) in BindToClose. Not only is that just freezing the thread, but realistically this doesn’t do anything. If BindToClose has nothing to run, it finishes and the instance is closed.

If code ever ends up needing to “buy” the maximum extent to what BindToClose allows, that’s a faulty implementation. BindToClose should only be used to wrap up any closing functions (such as data saving) and then the instance needs to be closed.


That’s all I have to say. Cheers for the tutorial.

40 Likes

Hey, thanks for the reply.

Good to know, I’ll keep that in mind. However I have seen instances of users in #help-and-feedback:scripting-support where the function called on player removing does not quite finish which is why I included it in the bullet point

That’s what I meant I just didn’t word it correctly, but the thread is yielded until the request is completed.

Was getting this from the little message you see at top when going to the api reference for “Async” labelled functions

This is a yielding function. When called, it will pause the Lua thread that called the function until a result is ready to be returned, without interrupting other scripts.

Probably not especially if it’s a very small amount of players, i don’t think it would take the whole 30 seconds to save one player’s data, but it’s best to account for them nonetheless, especially because web requests can take seconds, at least from my experience

7 Likes

Appreciated tutorial, will look into this when starting to make a new game. Definitely something I need. Lol

5 Likes

@colbert2677, Roblox was now updated, does it now still fire for the last player?

2 Likes

Great tutorial. Another good practice is to include an auto-save every couple of minutes. Doing this will make sure that in case of a data loss (worst-case scenario), the maximum damage done is the player losing only a few minutes of work/stats instead of their whole session.

9 Likes

i edited my post to clarify that a bit. I’ve seen posts on scripting support where data doesn’t save because the event listener doesn’t get called at all or it doesn’t finish completely. Either one of those two.

Yeah maybe, but in some games an auto save just might not work, and after all this isn’t complete guide to data store “best practices”, it was just for this one issue. But thank you!!

6 Likes

Yeah, i will try your tips to save my data (i have the same problem), but it really can help, thanks a lot!

3 Likes

Thanks for the tutorial, I knew the basics but I didn’t know about the :BindToClose() function, thank you! :smiley:

5 Likes

Sorry I had to resurface this,

but is it still possible for a player to leave and the data container to be removed before there is an opportunity to save it OnPlayerRemoving ?

To prevent that edge case, would simply cloning the container instantaneously after a player leaves and then saving that data be viable?

Typically no because PlayerRemoving fires before the Player instance is destroyed, but I don’t trust the unpredictability and potential bugs that Roblox undergoes so I never use the Player object as a dependency for my data folders. If I need a physical representation of my data I put that folder in a storage service or hold it as pure data in a data handler module.

If you run into the edge case of the folder being destroyed when PlayerRemoving is called, it’d be significantly better to rethink your structure altogether. Instead of accounting for the edge case, you can just not have it affect you at all by keeping the data folder off the Player object. Just remember to clean up your instances after saving.

8 Likes

This should be:

local cash_data_store = DataStoreService:GetDataStore("CashDataStore")

This is due to the fact that the DataStore Service is being defined as DataStoreService and not DataStore. Just stating this to prevent misinformation. :grin:

3 Likes

It is not misinformation, just a typo, but thank you

5 Likes

This worked for me but I have to add a very small wait at the end or else it won’t save

1 Like

I LOVE YOU SOO MUCH OMG! but i am suposse to use game bind to close and players removing at the same script? could u give me any example?

1 Like

A really useful tutorial for beginners.

Often, studio shuts down too fast and PlayerRemoving can’t fire. Simply saving everyones data would work but it would throw a “DataStore request added to queue” warning and freeze a bit (which isn’t bad, but still), that’s why I often use something like this if I want it to save in studio:

game:BindToClose(function()
    if RunService:IsStudio() then
        wait(1)
    else
        for i,player in ipairs(Players:GetPlayers()) do
            coroutine.wrap(save)(player)
        end
    end
end)

Basically make it wait 1 second if it is studio, to let PlayerRemoving fire.
If anyone knows any better way, feel free to reply.

8 Likes

Hi so I recently made a post about my Data not saving corretly and as of now I can’t develop right now but I can use the Forum. So usually what I do is do a « prepare » script where basically I make lines of codes and ask if they are correct before I use them.

So here’s what I did: