Skip to content

Backups

Copies DataDriver profiles into rotated backup keys once per interval game-wide, optionally mirrors every save, restores them when a record is corrupt, and reads them for support without taking a lock.

Realm
Server
Require
ServerScriptService.ChloeKernelServer.Data
Since
v0.3.0
Source
Data/init.luau

Backups are DataDriver’s optional retention in separate keys: rotated copies of each profile in a second backend, restored when the primary record is corrupt, and readable by support tooling without touching the player’s session. Since 0.9.0 the driver restores from Roblox’s free version history first, so backups are for retention you need to control: history beyond the version window, a copy fresher than the hourly version, or a copy off Roblox in your own database. What keeps them cheap is that only the lock holder writes a backup and the schedule is stored inside the record, so no coordinator or cross-server bookkeeping is needed.

Config.Backup turns backups on. Each key’s schedule lives in its own Meta as BackupAt and an incrementing BackupCounter, so it follows the player between servers, and a hundred servers touching the same key still produce one backup per interval.

During a save, when Clock() - Meta.BackupAt >= IntervalSeconds, the save claims the next slot: the counter goes up by one, the slot is ((Counter - 1) % Keep) + 1, and the new BackupAt is written inside the same atomic save as the data. The copy itself is written after the primary write lands, to the key {key}/backup/{slot}, so a backup is never newer than the record it backs up. The slots form a fixed ring: storage per player is bounded.

The claim commits only with a successful primary write. A primary save that fails leaves the backup due for the next save. A copy that fails after the primary landed is remembered and retried on the next save with fresh data, without taking a new claim.

With Mirror = true, saves also copy to a live slot, so the newest backup is at most one autosave old. MirrorIntervalSeconds spaces those copies out; a release always attempts one, and a failed mirror copy is retried on the next save. Combine the ring and the mirror for a fresh copy plus daily history.

A key that is not loaded anywhere is not backed up again. Its data is not changing, so its last backup is still current.

Backups are optional, so a copy never multiplies the primary save’s retry ladder: each copy is one update call. After a failed copy the backup backend cools down for 15 s, doubling with each consecutive failure up to 300 s. During the cooldown copies fail at once (backup backend is cooling down) and stay pending for a later save. One successful copy clears the cooldown. The cooldown is per driver.

Each backup record is { Data, Meta }. Data is the same encoded payload the primary save wrote, re-encoded between buffer and base64 form when the two backends disagree on buffer support. Plain-table payloads are the same frozen snapshot the primary write used. Meta carries:

Field Meaning
SchemaVersion, CreatedAt Copied from the profile.
At Copy time, from the driver’s Clock.
Counter The profile’s BackupCounter when the copy was written.
TransactionIds The profile’s trade replay ids, so a restore keeps replay protection.
UserIds The profile’s owners. A backup holds the same personal data, so the DataStore backend tags it for GDPR erasure like the primary. Before 0.9.0 backups were written without them.
DefaultPaths The profile’s reconcile ledger, so default entries the game had removed stay removed after a restore. Backups written before 0.9.1 do not have it.
Key, Source The profile key and the JobId of the server that wrote the copy.
Primary Backup.Backend unset Notes
"DataStore" (or unset) DataStore {Name}_Backups, with Backup.BackendConfig Name must be at most 42 characters: DataStore names stop at 50, and DataDriver.new errors on a longer backup name.
"Memory" A Memory backend Before 0.9.0 a Memory primary wrote its backups to the production DataStore.
"Http" or an instance DataStore {Name}_Backups Set Backup.Backend explicitly.

Backup.Backend accepts "Memory", "DataStore", "Http" or a backend instance. "Http" keeps copies entirely off Roblox.

local PlayerData = DataDriver.new({
Name = "PlayerData",
Defaults = { Coins = 0, Inventory = {} },
Backup = {
IntervalSeconds = 86400, -- one ring copy per key per day
Keep = 3, -- the last 3 dailies per key
Mirror = true, -- and a "live" copy of every save
MirrorIntervalSeconds = 300, -- at most one mirror copy per 5 minutes (releases always copy)
},
})

Config.Backup:

Field Default Description
Backend see Where backups go "Memory", "DataStore", "Http" or a backend instance.
BackendConfig nil Config for the named backup backend.
IntervalSeconds 86400 Per-key cadence of ring copies, once per interval game-wide.
Keep 3 Ring slots per key. Asserted to be at least 1.
Mirror false Also copy saves to the live slot.
MirrorIntervalSeconds 0 Minimum spacing between mirror copies. Releases always copy.
Fallback true Restore the newest readable backup when the primary is corrupt. false also turns the version fallback off unless Versions.Fallback is set.

When a load fails with a recoverable error (the record exists and is writable, but its data will not decode) and Backup.Fallback is not false:

  1. On DataStores the driver walks version history first. The newest readable backup is read up front, and if it is newer than every readable version, the version walk stops and the backup wins. A mirror copy is usually fresher than an hourly version.
  2. The backup restore scans the live slot and every ring slot and picks the newest readable one by At, with the counter breaking same-second ties. A slot that fails to read or decode is skipped; the others are still tried.
  3. The load runs again with that backup as the record: the lock check, migrations, reconcile (following the backup’s ledger) and Validate all apply, and the backup is written over the primary inside the same atomic transform. Meta gets RestoredFromBackupAt and RestoredFromSlot, and the restored copy’s At becomes the current BackupAt.
  4. profile.RestoredFromBackup is true on that load only. The driver warns <key> restored from backup slot <slot> (<n>s old), and the attached lifecycle publishes Kernel.ProfileRestored after Kernel.ProfileLoaded, so the game can tell the player or grant make-goods.

A restore follows every fail-closed rule. It never runs while another server holds a live lock (corrupt does not mean unlocked), never while the backend is down (loading without a lock is a duplication path), and never over a live record that Validate or a migration rejects.

-- Inspect a player's newest backup without touching their session
local Snapshot, Err = PlayerData:peekBackup(`Player_{userId}`)
if Snapshot then
print(`backup coins: {Snapshot.Coins}`)
end
for _, Entry in PlayerData:listBackups(`Player_{userId}`) do
print(`slot {Entry.Slot}: {os.time() - Entry.At}s old, schema v{Entry.SchemaVersion}`)
end

listBackups and peekBackup never lock or write, so they are safe while the player is online on another server.

Since April 9, 2026, DataStore storage above 500 MB + 1 MB x lifetime users is billed. Each ring slot and the mirror slot is a separate key whose latest version counts toward storage, so Keep = 3 plus Mirror can add up to four times the primary profile’s size. Native version history, which the driver restores from first, is free. Leave Backup off unless you need retention beyond the version history or a copy off Roblox.

Member Description
driver:backupNow(profile) -> (ok, err?) Forces a ring copy on the next write regardless of the schedule, then saves.
driver:listBackups(key) -> { { Slot, At, SchemaVersion } } Every readable slot for a key, newest first. Slot is a string ("live", "1", …). Empty when Backup is off.
driver:peekBackup(key, slot?) -> (data?, err?) A decoded deep copy of one backup’s data, or the newest readable one when slot is omitted. Never locks, never writes.
Topic Payload Fired
Kernel.ProfileRestored session, profile The attached lifecycle loaded a profile restored from a backup or a version.
  • Backups are written only by the server holding the lock, and only after a successful primary save. A profile whose saves are refused writes no backups.
  • A backup written before 0.9.1 does not carry the record’s reconcile ledger (Meta.DefaultPaths). Restoring one reconciles like a record from before the ledger, so dictionary entries from Defaults that the game had removed are filled in again, once.
  • Backup.Fallback = false also turns off the version fallback when Versions.Fallback is unset.
  • A restore rolls back one record. If the player traded after the backup was taken, their side of the trade is undone and the other participant keeps theirs.

ChloeKernel v0.9.1. Access to the framework is private and granted individually by Chloe. Games built with it credit CryptedChloes in their description.