Properly save data on close

A tutorial that teaches how to properly save player data on server close

by incapaz

Author Avatar

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(client)
    -- ...
end

local function save(client)
    -- ...
end

Players.PlayerAdded:Connect(function(client)
   load(client)
end)

Players.PlayerRemoving:Connect(function(client)
    save(client)
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 could 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):

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

What is the solution?

It's pretty simple. 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 _, client in ipairs(Players:GetPlayers()) do
        save(client)
    end
end)

And there you go. 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 functions to do their thing. NOT 30 SECOND EACH. 30 seconds for all of them to do their thing. When those 30 seconds are up, 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, data store requests are web requests, and 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.

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.

If you're unfamiliar familiar 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 the resumer.

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's managed by Roblox to resume.

Applying this knowledge into our code:

game:BindToClose(function()
    for _, client in ipairs(Players:GetPlayers()) do
        coroutine.wrap(save)(client)
    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.