Syntax
-- Save during normal operation; do not rely only on shutdown.
-- Track failures and keep pending data until acknowledged.Examples
Bound retries outside the update callback
ModuleScript named LessonRetry in ServerScriptService, alongside LessonSave. This retries that idempotent highest-progress merge at most three times. Failed attempts remain failures; the helper never changes ready=false into ready=true.
local LessonSave = require(script.Parent:WaitForChild("LessonSave"))
local LessonRetry = {}
function LessonRetry.save(userId, session)
for attempt = 1, 3 do
local ok, reason = LessonSave.save(userId, session)
if ok then return true end
warn("Progress save failed", attempt, reason)
if attempt < 3 then task.wait(2 ^ (attempt - 1)) end
end
return false
end
return LessonRetryUnderstand a shutdown hook in isolation
Server Script wiring fragment, not a complete persistence system. sessionsByUserId must be the same table populated by your successful load flow, not a second empty table. This demonstrates bounded parallel dispatch; platform shutdown can still interrupt outstanding requests.
-- Integration fragment: sessionsByUserId is owned by your session manager.
local LessonRetry = require(script.Parent:WaitForChild("LessonRetry"))
game:BindToClose(function()
local pending = 0
for userId, session in pairs(sessionsByUserId) do
if session.ready then
pending += 1
task.spawn(function()
local ok, saved = pcall(LessonRetry.save, userId, session)
if not ok or not saved then warn("Shutdown save unresolved", userId) end
pending -= 1
end)
end
end
local deadline = os.clock() + 20
while pending > 0 and os.clock() < deadline do task.wait(0.1) end
if pending > 0 then warn("Shutdown deadline reached with pending saves") end
end)Best practices
- Add staggered autosaves during normal play, bounded concurrency and request-budget awareness; never save every frame.
- Coordinate PlayerRemoving, autosave and shutdown so a session does not start overlapping writes. Snapshot or version pending changes and clear dirty state only for the version acknowledged.
- Retries can repeat a write after an uncertain response. Use idempotent merges or an operation-ID protocol, and monitor failures rather than promising guaranteed saves.
At a glance
- Purpose
- Typed scripting and Roblox development
- File extension
- .luau
- Runs in
- Luau host; Roblox engine examples require Roblox Studio
- Usually used with
- Roblox APIs and Studio
Specifications & further reading
Related Luau documentation
UpdateAsync lets a non-yielding callback transform the latest value. It may invoke that callback again after a conflict. This example saves a monotonic highest-lesson record with max; that merge rule is appropriate for progress that never decreases, not balances, inventories or resets.Load and validate player data
A successful read returning nil means no stored value exists. A failed read means the value is unknown. Those cases must never share a fallback that later saves defaults over real data. Validate stored fields before marking a session ready.task scheduling and RunService
Use task scheduling for delayed or deferred work, and RunService when a feature genuinely depends on simulation or rendering steps. A delay is a minimum scheduling interval rather than an exact clock. Avoid doing work every frame when an event can report the change.DataStore setup and safe Studio testing
DataStoreService persists data across sessions and is accessed by server scripts. Studio API access can reach real experience data. Use a separate test experience and test store names before enabling access; these examples are learning exercises, not a complete production persistence framework.