Backups
The backup subsystem is DataDriver’s disaster-recovery layer: rotated periodic copies of a profile’s record to a second location, with automatic restore when the primary is corrupt. The mechanism that makes it cheap: only the lock holder writes backups, and the schedule rides the record — no coordinator, no separate cron, no server-to-server awareness of who backed up what last.
Mental model
Section titled “Mental model”Config.Backup turns the feature on. A key’s backup schedule lives inside its own Meta — BackupAt and an incrementing BackupCounter — so it survives a player hopping between servers, and a hundred servers touching the same key can’t produce a hundred dailies:
During a save, if Clock() - Meta.BackupAt >= IntervalSeconds, that save also produces a backup copy, written after the primary write lands, to slot ((Counter - 1) % Keep) + 1 under the key {key}/backup/{slot} — a fixed ring, never growth. A failed copy is remembered (PendingBackupSlot) and retried on the next save with fresh data; the interval claim is not re-taken. A key that isn’t loaded anywhere doesn’t need re-backing-up: its data isn’t changing, so the last backup is still true.
With Mirror = true, every save additionally copies to a live slot, so the newest fallback is never older than one autosave — combine both for “fresh copy + daily history.”
Each backup record carries its own Meta: SchemaVersion, CreatedAt, At (copy time), Counter, Key, and Source (the writing server’s JobId). If the backup backend differs from the primary in buffer support, the payload is re-encoded for it; codec-less payloads are deep-copied so the stored copy is detached from the live table.
Configuring the schedule
Section titled “Configuring the schedule”local PlayerData = DataDriver.new({ Name = "PlayerData", Defaults = { Coins = 0, Inventory = {} }, Backup = { IntervalSeconds = 86400, -- one backup per key per day (the default) Keep = 3, -- rotate the last 3 dailies per key Mirror = true, -- ALSO copy every save to a "live" slot -- Backend = "Http", BackendConfig = { BaseUrl = ... }, -- external DB as the target },})Config.Backup:
| Field | Default | Description |
|---|---|---|
Backend |
DataStore "{Name}_Backups" |
"Memory", "DataStore", "Http", or a backend instance. "Http" keeps copies entirely off Roblox. |
BackendConfig |
nil |
Config for the named backup backend. |
IntervalSeconds |
86400 |
Per-key backup cadence — exactly once per interval game-wide, no coordination. |
Keep |
3 |
Rotation slots per key (asserted >= 1). Storage per player is bounded forever. |
Mirror |
false |
Additionally copy every save to a live slot, so the fallback is never older than one autosave. |
Fallback |
true |
Restore the newest readable backup when the primary record is corrupt. |
Automatic restore
Section titled “Automatic restore”When driver:load() hits a recoverable failure — the record exists and is writable but its data won’t decode or migrate — and Backup.Fallback isn’t false, the driver:
- Scans the
liveslot and allKeepring slots, picks the newest readable one byAt(the write counter breaks same-second ties). Corrupt slots are skipped; the rest are still tried. - Re-runs the load with that backup as the record: the backup is written over the primary inside the same lock-checked atomic transform.
MetagetsRestoredFromBackupAtandRestoredFromSlot, and the restored copy’sAtbecomes the currentBackupAt. - Flags
profile.RestoredFromBackup = true— transient, true only on the load that performed the restore — and the attached lifecycle publishesKernel.ProfileRestoredso you can tell the player, ping a webhook, or grant make-goods.
The restore honors every fail-closed rule: it never runs while another server holds a live lock (corrupt ≠ unlocked — the lock expires first, then the restore is allowed), and never when the backend is down, because loading without a lock is a dupe vector.
Ops tooling
Section titled “Ops tooling”-- Support workflow: inspect a player's newest backup without touching their sessionlocal Snapshot, Err = PlayerData:peekBackup(`Player_{userId}`)if Snapshot then print(`backup coins: {Snapshot.Coins}`)endfor _, Entry in PlayerData:listBackups(`Player_{userId}`) do print(`slot {Entry.Slot}: {os.time() - Entry.At}s old, schema v{Entry.SchemaVersion}`)endRoblox’s own point-in-time versioning (ListVersionsAsync) still works underneath as a third layer, since the driver only ever writes through UpdateAsync.
API reference
Section titled “API reference”| Member | Description |
|---|---|
driver:backupNow(profile) → (ok, err?) |
Forces a backup 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. |
driver:peekBackup(key, slot?) → (data?, err?) |
Read-only decoded deep copy of a backup (newest if no slot given). Never locks, never writes — safe while the player is online on another server. |
Bus topics
Section titled “Bus topics”| Topic | Payload | Fired |
|---|---|---|
Kernel.ProfileRestored |
session, profile |
The load restored from a backup — alert the player, grant make-goods. See Bus. |
Gotchas
Section titled “Gotchas”- Ops tooling is read-only and lock-free by design:
listBackupsandpeekBackupnever take the session lock, so support tooling can inspect a player’s backups while they’re online on another server. - Restore never bypasses session locks and never runs with the backup backend down — the same fail-closed doctrine that governs every other DataDriver load path applies here too.