Is it a good idea to :SetAsync() after each checkpoint?

Don’t use SetAsync() alot. I’d only use SetAsync() if they don’t have data. Other than that I’d reccomend to use UpdateAsync() and you should really only use it ever 30-60 seconds and if the player leaves.

Try to use this:

game.Players.PlayerRemoving:Connect(function(Player)
    local success, errormessage = pcall(function()
		obbyDS:SetAsync(plr.UserId .. "-obbyStageProgress", plr.leaderstats.Stage.Value)
    end)

    if not success then
        local count = 1
        repeat
            warn("[ERROR]: Error- saving " .. Player.Name .. "'s data failed.\nAttempt: " .. tostring(count) .. "\nError Message: " .. errormessage)
            success, errormessage = pcall(function()
                count += 1
		        obbyDS:SetAsync(plr.UserId .. "-obbyStageProgress", plr.leaderstats.Stage.Value)
            end)
            wait(5)
        until success or count >= 5
    end

Thank you everyone for the help. I’ve gone ahead and did a few things

@zachariapopcorn - I’ve actually added a button which saves data and has a cooldown of 2 minutes so players can only save their stage after 2 minutes
@SilentsReplacement - I added an autosave which saves everyone’s data every 2 minutes as well

And I also kept Player Removing to save when they leave, and removed SetAsync after each checkpoint.

I recommend rather using :UpdateAsync(). And if your checkpoints are close to each other, it’s a bad idea, it can break DataStores and delay saves and loads, which can cause data loss. Unless you’re using the DataStore2 module, it’s a bad idea.

Using simply :UpdateAsync() and keeping track if the server is shutting down is simply enough.
You can save every player’s data when the game is shutting down by using game:BindToClose(function() and saving each player’s data manually there by looping through them.

I recommend just save it when the player leaves. If you want, people are recommending to also save it every few minutes. Also, use UpdateAsync. It helps prevents data loss.