Save on leave, and back that up with periodic autosaves so a crash can't wipe an hour of progress.
Saving on leave works — until it doesn't. If a player's game crashes, the app closes, or the server shuts down unexpectedly, PlayerRemoving may never fire, and their progress is gone. The fix is autosave: write the data regularly, not just at the end.
PlayerRemoving fires when a player leaves the server:
Players.PlayerRemoving:Connect(function(player)
local coins = player.leaderstats.Coins.Value
coinsStore:SetAsync(player.UserId, coins)
end)
This is the safety net for clean exits — pressing Leave, or finishing a round. It's the moment you know the player is done, so it's the natural place to write their final state.
The player still exists during PlayerRemoving — you can read their leaderstats. Just don't try to show them anything; they're on their way out.
The event that fires when a player leaves the server is .
A clean exit isn't guaranteed. Autosave covers the gap: every N seconds, write everyone's data.
task.spawn(function()
while true do
task.wait(60) -- once a minute
for _, player in ipairs(Players:GetPlayers()) do
if player.leaderstats then
coinsStore:SetAsync(player.UserId, player.leaderstats.Coins.Value)
end
end
end
end)
Players:GetPlayers() — a table of everyone currently in the server.if player.leaderstats then guard skips players who haven't been set up yet.task.spawn keeps this loop running alongside the rest of the script — it never blocks the game.Why save periodically instead of only on PlayerRemoving?
Two habits keep autosave from causing trouble:
if player.leaderstats then — so you never save a nil folder.Inside a PlayerRemoving handler, write the line that saves the leaving player's coins to the store.
Players.PlayerRemoving:Connect(function(player)
-- save their coins
end)if player.leaderstats then so early joins don't write nil.Next: the uncomfortable truth about who you can trust.