DataStore library for Roblox

VirtualStore

A practical way to load player profiles, save changes, send safe data to the client, and recover work when a player disconnects or Roblox services fail.

  • 01 One server owns a loaded profile
  • 02 Important work survives disconnects
  • 03 Clients receive only approved paths
Now Claiming player lease
Why Only the stored lease owner may commit.

Start here

Save player data with one shared module.

For most games, VirtualStore.players is the right entry point. It opens Player_<UserId>, retries temporary load failures, releases the profile when the player leaves, and can replicate selected fields to the client.

ServerScriptService / PlayerData.server.luau
local ServerScriptService = game:GetService("ServerScriptService")
local VirtualStore = require(ServerScriptService.VirtualStore)

local Data = VirtualStore.players({
    Name = "PlayerData",
    Template = {
        Wallet = { Coins = 0 },
        Inventory = {},
    },
    Public = {
        {"Wallet", "Coins"},
        "Inventory",
    },
})

Data:Add(player, {"Wallet", "Coins"}, 100)
01
Open

Load the profile and make sure another server is not still using it.

02
Change

Use helpers or a transaction so invalid data does not reach the live profile.

03
Replicate

Send only the fields the player's UI needs.

04
Release

Save one last time, then let the next server open the profile.

Quick guide

Which API should I use?

Need Use Why
Load and save playersVirtualStore.playersUse this for the normal player join-to-leave lifecycle.
Change one valueSet / Add / IncrementSimple, validated, and easy to replicate.
Change several values togetherEdit / TransactionIf one check fails, none of the changes are kept.
Finish after a disconnectDurable operationStore the pending work so another session can finish it.
Save before teleport or checkoutCheckpointAsyncWait for a confirmed save before continuing.
Trade across profilesSaga / coordinatorTrack every step because Roblox cannot save both keys at once.

Project structure

A project layout you can actually maintain.

The beginner layout is enough for a small game. The advanced layout is for projects where several systems or developers share the same profile and need clear ownership.

Goal Keep one DataService and call it from your server scripts.

This is the simplest setup that stays safe. Server scripts call a few clear methods; clients read replicated values and ask the server to perform actions.

  • Server owns every mutation and reward value.
  • DataService is the only module that constructs VirtualStore.
  • Gameplay scripts never write session.Data directly.
  • Client scripts never receive the complete private profile.
Beginner Roblox hierarchy
ServerScriptServiceserver-only code
VirtualStoreinstalled package
DataServicecreates and exports Data
LeaderboardServiceprojects saved scores
CoinGiver.servertrusted gameplay action
Shop.servermulti-field transaction
ReplicatedStorageshared read/request boundary
Remotesclient requests only
VirtualStoreClientinstalled automatically
StarterPlayerScriptspresentation code
DataHud.clientobserves public replica
DataService

Defines template, public paths and operations once. Exports the ready player facade to every server gameplay script.

CoinGiver / Shop

Validates the RemoteEvent request, waits for loaded data and calls Add or Edit. It never decides based on client-provided reward values.

DataHud.client

Connects to the named replica and observes public paths. It cannot save or mutate server data.

LeaderboardService

Projects the saved authoritative score into OrderedDataStore and serves bounded top/rank queries.

DataService ModuleScript
local VirtualStore = require(ServerScriptService.VirtualStore)

local Data = VirtualStore.players({
    Name = "PlayerData",
    Template = {
        Wallet = { Coins = 0 },
        Inventory = {},
    },
    Replication = {
        Name = "PlayerData",
        Paths = {{"Wallet", "Coins"}, "Inventory"},
    },
})

return Data
CoinGiver.server Script
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local Data = require(ServerScriptService.DataService)
local claimRemote = ReplicatedStorage.Remotes.ClaimCoins
local lastClaim = {}

claimRemote.OnServerEvent:Connect(function(player)
    local now = os.clock()
    if now - (lastClaim[player] or 0) < 1 then return end
    lastClaim[player] = now

    local _, loadError = Data:Wait(player, 5)
    if loadError then return end

    local ok, err = Data:Add(
        player,
        {"Wallet", "Coins"},
        10,
        {Source = "CoinGiver"}
    )
    if not ok then warn(err.Code, err.Message) end
end)
DataHud.client LocalScript
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Client = require(
    ReplicatedStorage:WaitForChild("VirtualStoreClient")
)
local Replica = Client.connect("PlayerData")

Replica:Observe({"Wallet", "Coins"}, function(coins)
    coinsLabel.Text = tostring(coins)
end)
Goal Give each system a clear part of the profile.

Use this when economy, inventory, progression and operations are large enough to be maintained separately. Each module gets a clear job and fewer reasons to touch unrelated data.

  • Bootstrap wires dependencies; it does not contain gameplay.
  • Each domain owns a limited set of profile paths.
  • Cross-server delivery enters through deterministic operations.
  • Cross-key economy enters through a coordinator and journal.
Advanced Roblox hierarchy
ServerScriptService / Datapersistence composition
Bootstrap.serverstarts Data and services
PlayerDataexports configured facade
Schematemplate + schema version
Migrationsold shape conversions
Operationsdeterministic durable handlers
Replicationpublic path policy
OrderedIndexoptional sorted projection
ServerScriptService / Domainsgameplay ownership
EconomyServiceWallet paths
InventoryServiceInventory paths
ProgressionServiceProgress paths
TradeCoordinatorjournal + saga + repair
LeaderboardServiceprojection + sorted queries
ServerScriptService / Admintrusted incident tooling
DataRepairServiceauthorized repair calls
StarterPlayerScripts / Controllersclient presentation
DataControllerreplica observers
ModuleMay writeMust not own
EconomyServiceWalletInventory grants, admin repair
InventoryServiceInventory / EquippedCurrency pricing, trade journal
ProgressionServiceProgress / QuestsPurchase fulfillment
OperationsDeclared handler pathsYielding or external side effects
TradeCoordinatorJournal + queued intentsDirect multi-key atomic claims
LeaderboardServiceOrdered projection onlyAuthoritative economy decisions
DataRepairServiceAuthorized exact keyClient-controlled input
PlayerData ModuleScript
local ServerScriptService = game:GetService("ServerScriptService")
local VirtualStore = require(ServerScriptService.VirtualStore)
local Schema = require(script.Parent.Schema)
local Operations = require(script.Parent.Operations)

return VirtualStore.players({
    Name = "PlayerData",
    SchemaVersion = Schema.Version,
    Template = Schema.Template,
    Migrations = Schema.Migrations,
    Operations = Operations,
    Replication = require(script.Parent.Replication),
    Settings = {
        StrictData = true,
        TrackChangeDetails = true,
        AuditLogLimit = 50,
    },
})
EconomyService ModuleScript
local EconomyService = {}
local Data
local VirtualStore

function EconomyService.Start(data, virtualStore)
    Data = data
    VirtualStore = virtualStore
end

function EconomyService.Purchase(player, itemId, price)
    return Data:Edit(player, function(profile)
        if profile.Wallet.Coins < price then
            return VirtualStore.Cancel
        end

        profile.Wallet.Coins -= price
        table.insert(profile.Inventory, itemId)
    end, {
        Reason = "Purchase",
        Source = "EconomyService",
    })
end

return EconomyService
Operations ModuleScript
return {
    GrantMatchReward = function(profile, payload)
        assert(type(payload.Coins) == "number")
        profile.Wallet.Coins += payload.Coins
        return {Granted = payload.Coins}
    end,
}
Bootstrap.server Script
local ServerScriptService = game:GetService("ServerScriptService")
local VirtualStore = require(ServerScriptService.VirtualStore)
local Data = require(script.Parent.PlayerData)
local Economy = require(ServerScriptService.Domains.EconomyService)
local Inventory = require(ServerScriptService.Domains.InventoryService)

Economy.Start(Data, VirtualStore)
Inventory.Start(Data, VirtualStore)

Data.Store.CriticalStateChanged:Connect(function(critical)
    Economy.SetPurchasesEnabled(not critical)
end)
Rule 01Construct once

Create the player facade in one ModuleScript. Requiring that module returns the same configured data surface to every domain.

Rule 02Mutate through owners

Remote handlers call domain services. They do not directly reach into arbitrary profile paths.

Rule 03Operations are pure

Handlers validate payload facts, mutate one profile and return a result. They never yield or call external services.

Rule 04Admin stays isolated

Repair capabilities live in server-only tooling and never cross a client RemoteEvent or RemoteFunction.

How it works

The parts worth knowing.

You do not need every feature on day one. Read the first few topics before starting, then come back when your game needs offline rewards, repair tools, or multi-key workflows.

New here? Read the first six in order. The rest can wait until you need them.

01Player facade and core StoreChoose the entry point
Short version

VirtualStore.players is the convenient player lifecycle layer. VirtualStore.new is the lower-level key-based Store.

What happens

The player facade opens Player_<UserId>, associates GDPR IDs, tracks players, releases sessions and exposes short mutation helpers over a normal Store.

Good for

Use players for ordinary player profiles. Use the core Store for guilds, auctions, global records, custom keys or infrastructure code.

Watch out

The facade is not a second persistence system. Advanced work remains available through Data.Store.

02Record envelope, template and schemaWhat a saved record contains
Short version

Every key stores gameplay Data beside internal Meta, a schema version and a VirtualStore format marker.

What happens

The template supplies missing defaults. Metadata holds the lease, revisions, user IDs, tags, write receipts and operation ledgers without mixing them into gameplay data.

Good for

Put persistent gameplay state in the template: currency, inventory, settings and progression. Treat metadata as framework-owned.

Watch out

A template is not a fallback for a failed load. VirtualStore fails closed instead of replacing unknown stored data with defaults.

03Lease ownershipPrevent stale server saves
Short version

A lease is an expiring ownership token stored with one key. It identifies the only server allowed to save that active profile.

What happens

Open claims a token inside UpdateAsync. Every later write verifies the same stored token. Another server may claim only after release or expiry.

Good for

Leases automatically protect player joins, teleports, reconnects and any record that must have one active writer.

Watch out

A lease is not a gameplay resource lock and should not be force-released casually; another server may still have unsaved RAM changes.

04Active SessionWork with live profile state
Short version

A Session is the server-memory view of one opened record, with safe mutation, save, operation and release methods.

What happens

It moves through explicit states such as Active, Releasing, Released and LeaseLost. Only an Active owner may mutate or commit.

Good for

Use the active session for gameplay involving an online player or an opened custom record.

Watch out

Memory is not durable by itself. A full server-process crash can lose changes that were never checkpointed or represented as durable operations.

05StrictData and path mutationsMake normal changes observable
Short version

StrictData makes public profile data read-only. Changes go through Set, Patch, Increment, Insert, Remove or a transaction.

What happens

Safe helpers copy and validate the change, mark the session dirty, record changed paths and notify replication and audit listeners.

Good for

Use path helpers for small focused changes such as adding coins, changing a setting or inserting an item.

Watch out

Direct writes to non-strict session.Data cannot immediately emit paths and can bypass validation, audit and replication behavior.

06Transaction and EditCommit several changes together
Short version

A transaction is an isolated working copy for several related changes inside one profile.

What happens

The callback edits the copy. Callback failure, cancellation or invalid data discards it; success validates and swaps the completed copy into live session memory.

Good for

Use it for shop purchases, crafting, equipping, reward claims and any action where all profile changes must succeed or none should.

Watch out

A local transaction is not a DataStore write and cannot atomically commit several different keys.

07Named locks and lock transactionsCoordinate local systems
Short version

A named lock prevents two systems in the same server process from editing the same logical resource simultaneously.

What happens

WithLockTransaction holds the name, edits an isolated copy and commits only after the callback succeeds. Multi-session helpers acquire locks in deterministic key order.

Good for

Use it when inventory, crafting and trade code could race over one active session or several locally open participants.

Watch out

Local locks do not protect another server and do not make later writes to separate DataStore keys atomic.

08Dirty state, autosave, checkpoint and releaseDecide when to save
Short version

Dirty state means gameplay data changed since the last confirmed commit. Autosave persists it later; checkpoint persists it now; release performs the final save and clears the lease.

What happens

Autosaves are staggered and concurrency-limited. Clean sessions skip unnecessary data writes until lease renewal is due. Release saves final data and unlocks in one write.

Good for

Rely on autosave for routine progress. Checkpoint before teleport or a risky external flow. Release when ownership must end.

Watch out

A manual save is not needed after every mutation. Revision counts commits; DataRevision counts persisted gameplay-data changes.

09Write receiptsRecover ambiguous save responses
Short version

A write receipt is a bounded record of a unique commit token and whether that commit changed gameplay data.

What happens

If DataStore commits a write but the response is lost, retrying with the same token recognizes the completed commit instead of applying its transform twice.

Good for

VirtualStore uses this automatically for saves, releases and updates where a network failure can make the result uncertain.

Watch out

Receipts are intentionally bounded. They prevent short-term retry ambiguity, not permanent business-request idempotency.

10Durable operationsFinish work after disconnect
Short version

A durable operation is uniquely identified pending work stored inside the target profile.

What happens

A deterministic non-yielding handler mutates profile data. The result receipt is committed with the same single-profile write, so retries can see whether it already applied.

Good for

Use it for developer products, crate outcomes, matchmaking rewards, offline grants and work that must survive disconnects, teleports or temporary outages.

Watch out

Do not perform external side effects or random selection inside a retryable handler. Decide random outcomes before queueing and include them in the payload.

11Operation receipts, dedupe, TTL and dead lettersKeep failed work for repair
Short version

A receipt records terminal operation status. Dedupe tombstones remember completed IDs longer. TTL expires stale work. Dead letters retain bounded failed-operation details.

What happens

Attempts, timestamps and errors update during retries. The attempt limit or TTL produces a Failed receipt and dead letter. Reusing a retained ID returns its receipt instead of applying again.

Good for

Use status and dead letters to drive purchase retry, compensation, incident investigation and admin repair.

Watch out

All ledgers are bounded. Never reuse operation IDs. Replay a failed operation under a new globally unique ID.

12Owner-only client replicationChoose client-visible fields
Short version

Replication is a read-only client mirror containing only server-approved public paths or transformed output.

What happens

The owner receives a revisioned initial snapshot, then path patches from safe mutations. A missed revision requests a fresh snapshot.

Good for

Use it for HUD currency, settings, level, XP, inventory views and quest progress that the owning client must display.

Watch out

Publishing a parent can expose every child. Prefer narrow nested paths and never expose receipts, escrow, anti-cheat flags or admin data.

13Validation and schema migrationsUpgrade old profiles safely
Short version

Validation rejects values Roblox cannot safely store. Migrations convert an older known schema into the current schema one destination version at a time.

What happens

Open and offline update run required migrations, reconcile template defaults and validate before allowing the new record to commit.

Good for

Use migrations when renaming fields, changing inventory formats, splitting values or introducing a new persistent model.

Watch out

Migration is not automatic corruption repair. Each migration must be deterministic and tested against real older shapes.

14DataStore and Memory adaptersTest without live DataStore
Short version

An adapter implements storage requests. The production adapter talks to Roblox DataStore; the memory adapter provides deterministic local storage for tests.

What happens

Core lifecycle and correctness code call the adapter contract instead of directly depending on live DataStore behavior.

Good for

Use the memory adapter for automated tests, retry simulation, lease conflicts, operation failure cases and scheduler benchmarks.

Watch out

A memory benchmark does not measure Roblox network speed. Published-server comparisons need identical real conditions.

15Budgets, scheduler, diagnostics and critical stateWatch budgets and failures
Short version

Budget waiting controls request timing, the scheduler spreads autosaves, diagnostics count system behavior and critical state summarizes repeated recent issues.

What happens

Requests wait for their correct Roblox budget. Autosaves use capped concurrency. Metrics record requests, retries, waits, latency, failures, restores, deletes and lease loss.

Good for

Use diagnostics for dashboards, alerting, load testing and deciding whether trading or purchases should temporarily degrade during an incident.

Watch out

Higher concurrency is not automatically faster. Measure diagnostics before changing safety-oriented defaults.

16MessagingService handoffA useful hint, not a guarantee
Short version

MessagingService sends hints that a waiting server wants a lease or that new operation work exists.

What happens

A message can encourage faster save, release or processing, while DataStore records, polling, autosave and lease timeout remain the correctness path.

Good for

Enable it to reduce reconnect, teleport handoff and offline-delivery latency across servers.

Watch out

Messaging delivery is not guaranteed. Code must remain correct when every message is dropped.

17Version history, restore and deleteRecover or remove exact records
Short version

Version APIs inspect historical DataStore versions, restore one as a new latest version or create a deletion tombstone after exclusive ownership.

What happens

Restore and delete reject active foreign leases. Restore creates another version, preserving the state that existed before restoration.

Good for

Use it for administrator rollback, investigating lost progress and deliberate privacy/deletion workflows.

Watch out

Restore is not safe while another server actively owns the profile, and Roblox controls how long old versions remain available.

18Authorized admin repairInspect and repair exact keys
Short version

Admin repair is a server-only, key-based set of inspection and recovery methods protected by an explicit AdminAuthorize callback.

What happens

Authorized tooling can inspect metadata, list pending work and dead letters, cancel or fail work, requeue with a new ID, compact ledgers or force-release an exact inspected lease token.

Good for

Use it from trusted incident tooling when normal retry and lease timeout are not enough.

Watch out

Never expose a generic repair remote to clients. Dead-letter payloads and lease tokens contain sensitive operational data.

19Optional gameplay patternsReady-made helpers you can remove
Short version

Patterns are removable gameplay modules for cooldowns, daily rewards, purchase receipts, offline rewards and timed effects.

What happens

They prepare template fields and deterministic handlers, then call the same public transactions and durable operations as custom game code.

Good for

Use them to ship common systems quickly without designing their retry and idempotency behavior from scratch.

Watch out

Patterns are not correctness-critical core. They can be deleted, and they never replace leases, persistence or operation receipts.

20OrderedIndex leaderboardsKeep a sortable copy
Short version

An optional OrderedDataStore extension that stores integer projections and reads sorted pages without using VirtualStore's profile envelope.

What happens

Writes use OrderedDataStore budgets and bounded retries. Top queries page through sorted results; rank lookup scans only up to a configured maximum.

Good for

Use it for persistent scoreboards, fastest times, season points and other sortable integer read models.

Watch out

The projection is not atomic with the source profile and may lag. Never use it as the authoritative source for currency, purchases or trades.

21Trade saga, journal and escrowRecover multi-profile trades
Short version

A saga is a persistent multi-step workflow used because Roblox cannot atomically commit several DataStore keys. A journal records progress; escrow reserves assets before settlement.

What happens

The coordinator writes pending state, queues unique idempotent operations for each profile, observes receipts and retries, compensates or marks NeedsRepair when settlement cannot finish.

Good for

Use it for player trades, auctions, guild banks, ownership transfers and any valuable cross-profile economy action.

Watch out

withLockTransactions only protects local memory. It is not a substitute for a persistent coordinator across DataStore keys.

Safety notes

What VirtualStore protects, and what it cannot.

Inside one DataStore key, VirtualStore can make useful guarantees. Across several keys or external services, Roblox does not give us a real transaction.

Handled by VirtualStoreInside one profile
  • One active lease owner per key inside UpdateAsync.
  • Stale servers cannot overwrite a newer owner.
  • Final save and lease release share one atomic write.
  • Safe transactions roll back callback failures in memory.
  • Durable operation data and receipts commit together.
  • Write receipts recover ambiguous post-write responses.
  • Client replicas are owner-only, revisioned and path-filtered.
  • Admin repair fails closed without explicit authorization.
You must design for thisOutside one profile
  • Atomic commits across multiple DataStore keys.
  • Permanent idempotency after bounded ledgers prune old IDs.
  • Recovery of unsaved RAM-only changes after a process crash.
  • Instant cross-server visibility or MessagingService delivery.
  • Security of client-exposed admin capabilities or transforms.
  • Rollback for direct mutations made outside strict mode.
  • Automatic repair of every incomplete external workflow.
  • Global scanning of every DataStore key.

Revision means commit count. Use Metadata.DataRevision when only persisted gameplay-data changes matter.

When something fails

What the library does next.

Load outage

Do not open blank data

The load is retried with the same token. If the stored profile cannot be read safely, the player is not allowed into gameplay.

Save response lost

Check whether it already saved

A recent write receipt lets a retry recognize a commit whose response was lost.

Player disconnects

Leave the reward pending

Durable work remains in the profile so a later session can finish it.

Repeated failure

Stop retrying and report it

After the retry limit, the operation becomes Failed and its details are kept for repair.

Durable reward after matchmaking or teleport
Data:DeliverAsync(userId, `match:${matchId}:reward`, "GrantMatchReward", {
    Coins = 250,
})

Client data

Send the UI only what it needs.

Mark exact paths as public. If you publish {"Wallet", "Coins"}, the client does not receive reserved currency, trade escrow, or other fields beside it.

Server profile
Wallet
Coinspublic
ReservedCoinsprivate
TradeEscrowprivate
Progress
Levelpublic
XPpublic
AdminNotesprivate
Owner client
Wallet
Coins240
Progress
Level7
XP65
Server whitelist
Replication = {
    Paths = {
        {"Wallet", "Coins"},
        {"Progress", "Level"},
        {"Progress", "XP"},
    },
}
Client observer
Data:Observe({"Wallet", "Coins"}, function(coins)
    coinsLabel.Text = coins
end)

Leaderboards

Keep the real score in the profile.

OrderedIndex copies a saved integer into OrderedDataStore so you can sort it. If that copy is late or fails, the player's real score is still safe in VirtualStore.

Source of truth VirtualStore profile

This is the value your game trusts when granting rewards or checking purchases.

Data.Wallet.Coins = 1250
Sorted read model OrderedIndex

This copy exists only so the game can show top lists and look up nearby ranks.

"820815974" = 1250
1Mutate profile

Gameplay uses Data:Add or a transaction. OrderedDataStore is not touched yet.

2Confirm save

Session save succeeds, proving the authoritative profile value reached DataStore.

3Project integer

BindSession writes the selected path using OrderedDataStore budgets and bounded retries.

4Read sorted view

GetTopAsync returns ranked entries; GetRankAsync scans only within its configured bound.

Beginner: project after successful saves
local Coins = VirtualStore.OrderedIndex.new({
    Name = "CoinsLeaderboard",
    Ascending = false,
    Settings = { CacheTTL = 15 },
})

Data.Loaded:Connect(function(player, _profile, session)
    Coins:BindSession(
        session,
        tostring(player.UserId),
        {"Wallet", "Coins"},
        {ProjectNow = true}
    )
end)
Advanced: top list and bounded rank
local top, topError = Coins:GetTopAsync(100, {
    MinValue = 1,
    PageSize = 50,
})

local rank, rankError = Coins:GetRankAsync(
    tostring(player.UserId),
    {MaxScan = 1000}
)

if rankError and rankError.Code == "RankScanLimit" then
    print("Player is outside the scanned leaderboard range")
end

Do not add Coins in two places. Change profile Coins in VirtualStore, then copy the final value with SetAsync, ProjectAsync or BindSession. IncrementAsync is for a separate counter owned by OrderedDataStore. It is not retried automatically because a lost response could otherwise add the value twice.

Optional helpers

Common systems you can keep or remove.

These helpers use the public API. Delete them when your game does not need them; the core library keeps working.

01

Cooldowns

Saved timestamps and isolated session transactions for local gameplay gates.

Data.Cooldowns
02

Daily rewards

UTC day numbers, deterministic IDs and server-selected rewards.

Data.Daily
03

Purchase receipts

ProcessReceipt grants only after a durable operation reaches Applied.

Data.Purchases
04

Offline rewards

Server-calculated duration and idempotent reward delivery.

Data.Offline
05

Timed effects

Refresh, extend, stack, ignore and replace with durable queue helpers.

Data.Effects

Trading

A safe trade needs a recovery plan.

Roblox cannot save two player profiles in one atomic write. For valuable trades, keep a journal, reserve the offered assets, and make every settlement step safe to retry.

1Negotiate

Both offers are server validated. Changing an offer clears acceptance.

2Reserve

Each profile moves offered coins into durable escrow.

3Settle

Idempotent operations release escrow and grant incoming coins.

4Recover

Incomplete workflows remain visible as pending or NeedsRepair.

Inside the package

Where the main responsibilities live.

Public facade VirtualStore.luau

Factories, constants and dependency wiring. No Store method implementations.

Storeleases, commits, versions, repair, close
Sessiontransactions, locks, save, release
RecordCodecenvelopes, migrations, validation, receipts
OperationLedgerretry, dedupe, dead letters, compaction
AutoSaveSchedulerstaggering and concurrency
Diagnosticsmetrics and critical state
Replicationowner-only snapshots and patches
OrderedIndexoptional integer projections and sorted queries
AdaptersDataStore and deterministic memory tests

Examples

How the pieces work together.

These are the situations that usually cause data bugs. Each walkthrough shows the normal path and what happens when a request, server, or player disappears.

Routine profile

Player joins, earns coins and leaves

players + Add + autosave + release
1Open

VirtualStore.players receives PlayerAdded and repeatedly attempts to claim Player_<UserId> with one stable lease token.

2Become ready

After migration, reconciliation and validation, state becomes Ready. Gameplay may now read or mutate the profile.

3Mutate

Data:Add(player, "Coins", 100) validates the new number, marks dirty and emits the Coins path for replication.

4Persist

Autosave commits dirty data later. On leave, release saves final data and clears the lease in the same single-key write.

Recovery: temporary load errors retry; save failures keep the Session active; failed release does not pretend ownership ended.
Atomic profile action

Player purchases an inventory item

Edit / Transaction
1Validate request

Server code selects the item and trusted price. The client only requests an action; it does not author currency or reward values.

2Edit a copy

The transaction checks balance, subtracts coins and inserts the item into an isolated working copy.

3Commit together

If any check fails, return VirtualStore.Cancel. Nothing changes. If validation succeeds, all profile changes enter live memory together.

4Replicate paths

The owner client receives the changed public currency and inventory paths, while private purchase metadata remains server-only.

Use this for: shops, crafting, equipping, upgrade costs and any action that changes several fields in one profile.
Durable delivery

Crate result must survive disconnect

DeliverAndWaitAsync + receipt
1Choose outcome

The server selects the random reward before queueing. The stable outcome enters the operation payload.

2Queue unique work

A globally unique request ID creates pending work inside the profile. Reusing a retained ID returns the existing status.

3Apply once

The deterministic handler consumes the key and grants the selected reward. Data and the Applied receipt commit together.

4Present final result

Only show the final opening after the Applied receipt. A disconnect leaves pending work available for later processing.

Recovery: bounded retries become a Failed receipt and dead letter; compensation or admin repair can then act on visible failure.
Temporary outage

DataStore fails while the player is entering

stable-token retry + fail closed
1Do not create blanks

A failed request never causes VirtualStore to open the template as if no stored record existed.

2Retry one claim

Load attempts reuse the same token, allowing an ambiguous successful lease claim to be recognized safely.

3Expose state

The player facade moves through Loading and Retrying. Game code waits for Ready instead of guessing.

4Fail closed

After the configured attempts, loading becomes Failed and the default player facade kicks with a clear retry message.

Why: letting a player continue with blank data during an outage is much more dangerous than refusing the session.
Multi-key economy

Two players trade valuable assets

locks + journal + escrow + saga
1Negotiate locally

Server-authoritative offers and deterministic local locks prevent concurrent same-server edits while both players accept.

2Write pending journal

A persistent trade ID records participants, offers and workflow status before settlement begins.

3Reserve then settle

Unique per-profile durable operations move assets to escrow, then release outgoing value and grant incoming value.

4Recover incomplete work

The coordinator observes receipts, retries missing steps and marks permanent failures for compensation or NeedsRepair.

Boundary: Roblox has no multi-key atomic commit. A local multi-session transaction alone is never enough for a production trade.

API reference

Public methods, with context.

For most gamesStart with VirtualStore.players.

Keep one shared DataService, wait until the player is ready, then use Add, Set or Edit. Autosave and release handle the routine saves.

When you need more controlUse the API that owns the record.

Session methods are for loaded profiles. Store methods are for exact offline keys. Durable operations are for work that must resume later.

On every failureCheck the second return value.

Storage and mutation calls usually return valueOrSuccess, virtualError. Stop the action and log the error code rather than assuming it worked.

Player facadeVirtualStore.players
How to use itNormal server gameplay
local ServerScriptService = game:GetService("ServerScriptService")
local Data = require(ServerScriptService.DataService)

local _, loadError = Data:Wait(player, 5)
if loadError then
    warn(loadError.Code, loadError.Message)
    return
end

local ok, mutationError = Data:Add(
    player, {"Wallet", "Coins"}, 100,
    {Reason = "QuestReward", Source = "QuestService"}
)
if not ok then warn(mutationError.Code) end

Beginner: this is the default surface for player data. Do not manually open another Store for the same player key.

Advanced: wrap this facade inside domain services so RemoteEvents cannot call arbitrary paths or choose reward amounts.

Wait / Await

Waits until a player's load state reaches Ready or fails. Use before gameplay reads profile data.

IsLoaded / GetSession

Checks readiness or returns the active underlying Session for advanced server code.

Get / GetPath

Reads the complete profile or one string/nested-array path without exposing mutation.

Add / Increment

Adds a number at one path. Use for currency, XP and numeric counters.

Set / Patch

Replaces one path or computes its next value with a callback.

Insert / Remove

Safely changes an array-like path such as inventory or unlocked rewards.

Edit / Update

Runs a one-profile transaction for several related changes and complete rollback on failure.

WithLock / WithLockTransaction

Coordinates a named local resource; the transactional form also rolls back callback failures.

DeliverAsync

Queues one durable operation for an online player or offline UserId and returns without waiting for completion.

DeliverAndWaitAsync / GetDeliveryAsync

Waits for or checks the operation receipt when gameplay must know the terminal result.

CheckpointAsync / SaveAsync

Persists current memory while keeping the player session active. Checkpoint communicates a deliberate durability boundary.

ReplicateNow

Forces a fresh public snapshot when custom server code needs immediate replica refresh.

ReleaseAsync / CloseAsync

Releases one player or closes the entire facade and its underlying Store.

Core storeVirtualStore.new
How to use itExact offline key update
local snapshot, updateError = Data.Store:UpdateAsync(
    "Player_123",
    function(profile)
        profile.Wallet.Coins += 250
    end
)

if updateError then
    warn(updateError.Code, updateError.Message)
end

Use Store methods when: you know the exact key and no local active Session should own the edit.

Critical rule: offline update transforms may run multiple times and must not yield or perform external side effects.

OpenAsync

Claims a lease and creates one active Session. Use when this server needs ongoing ownership and writes.

ViewAsync

Reads a stored snapshot without claiming an active session. Use for read-only server tools and inactive records.

UpdateAsync

Atomically edits an offline record with no active lease. Its non-yielding transform may run more than once.

QueueOperationAsync

Writes uniquely identified durable work into a target profile, even while the owner is offline.

GetOperationAsync / AwaitOperationAsync

Reads or waits for operation status without exposing dead-letter payloads.

ListVersionsAsync / ViewVersionAsync / ViewAtTimeAsync

Inspects historical Roblox DataStore versions without changing the current record.

RestoreVersionAsync

Restores a historical snapshot as a new latest version after confirming no active lease blocks it.

GetDeleteConfirmation / DeleteAsync

Requires explicit exact confirmation, claims exclusive ownership and creates a deletion tombstone.

GetDiagnostics / IsCritical

Returns request metrics and whether recent issue thresholds place the Store in critical state.

GetSession / IsOpen / CloseAsync

Inspects local ownership or performs bounded shutdown release for all active sessions.

Active sessionSession
How to use itValidated multi-field purchase
local session = Data:GetSession(player)
if not session then return end

local ok, purchaseError = session:Transaction(
    function(profile)
        if profile.Wallet.Coins < 500 then
            return VirtualStore.Cancel
        end
        profile.Wallet.Coins -= 500
        table.insert(profile.Inventory, "Sword")
    end,
    {Reason = "Purchase", Source = "ShopService"}
)

Beginner: prefer facade methods unless you need Session-specific signals, locks, tags or manual operation processing.

Advanced: a Session is live memory under one lease. It is the correct surface for active-player domain services.

IsActive / GetState

Checks whether this Session still owns the lease and may safely mutate.

GetData / Get / Snapshot

Reads current memory or produces a copied snapshot for inspection.

GetMetadata / GetTag / SetTag

Reads public metadata or manages small record tags separate from gameplay data.

AddUserId / RemoveUserId

Associates Roblox user IDs with the record for GDPR-aware persistence.

Reconcile

Adds missing template defaults through the normal safe-mutation path.

Transaction

Edits a copied profile and commits only when callback and validation both succeed.

Set / Patch / Increment / Insert / Remove

Performs focused validated mutations and emits exact changed paths.

IsDirty / MarkDirty / MarkClean

Inspects or explicitly controls whether gameplay data requires another persistence commit.

GetDiff / GetAuditLog

Reads optional change details and recent audit entries when their settings are enabled.

WithLock / WithLockTransaction / IsLocked

Coordinates local logical resources; use the transactional form for rollback.

QueueOperationAsync / ProcessOperationsAsync

Queues work for this profile or explicitly processes pending operation handlers.

SaveAsync / CheckpointAsync / ReleaseAsync

Persists memory, marks a known durability boundary or ends lease ownership.

Authorized server toolsAdmin repair
How to use itInspect then repair one exact key
local ADMIN_CAPABILITY = {}
local Store = VirtualStore.new({
    Name = "PlayerData",
    Template = {},
    AdminAuthorize = function(_action, _key, context)
        return context == ADMIN_CAPABILITY
    end,
})

local deadLetters = Store:ListDeadLettersAsync(
    "Player_123", ADMIN_CAPABILITY
)

Safe order: inspect, understand, prepare a repair plan, then perform the smallest exact-key action.

Never: send the capability, lease token, dead-letter payload or generic repair method through a client remote.

InspectMetadataAsync

Reads internal metadata, including the exact current lease token, after authorization.

ListPendingOperationsAsync

Lists unfinished durable work for one exact key.

ListDeadLettersAsync

Returns retained failure details and payloads allowed by the configured byte limit.

ClearPendingOperationAsync

Creates a terminal Cancelled receipt and dedupe tombstone instead of silently forgetting the ID.

ForceFailOperationAsync

Turns pending work into a Failed receipt and dead letter for deliberate investigation.

RequeueDeadLetterAsync

Queues the retained payload under a new globally unique ID while preserving the original failure.

ForceReleaseLeaseAsync

Releases only the exact inspected token; changed or guessed tokens are rejected.

CompactLedgersAsync

Normalizes and prunes bounded ledgers, but refuses to run while an active lease may need its receipts.

Optional sorted projectionVirtualStore.OrderedIndex
How to use itSaved profile score leaderboard
local Leaderboard = VirtualStore.OrderedIndex.new({
    Name = "SeasonPoints",
    Ascending = false,
    Settings = {
        CacheTTL = 15,
        MaxRankScan = 1000,
    },
})

Leaderboard:BindSession(
    session,
    tostring(player.UserId),
    {"Progress", "SeasonPoints"}
)

Projection rule: the source profile remains authoritative. A leaderboard write can fail or temporarily lag without changing the player's real value.

Rank rule: OrderedDataStore has no direct rank lookup. GetRankAsync scans sorted pages and stops at MaxScan.

new(config)

Creates an integer-only OrderedDataStore projection with sort direction, optional value range, retry, budget, cache and scan settings.

SetAsync / ProjectAsync

Replaces one projected integer value. Prefer this for profile-backed scores.

IncrementAsync

Atomically increments an independent OrderedDataStore counter. It makes one attempt and returns AmbiguousIncrement on failure because automatic retry could apply the increment twice.

GetAsync / RemoveAsync

Reads or removes one exact projected key.

GetTopAsync

Reads ranked sorted entries with optional direction, page size, minimum, maximum and cache bypass.

GetRankAsync

Scans sorted pages up to MaxScan; returns RankScanLimit when the key lies outside the bounded scan.

ProjectSessionAsync

Reads one integer path from a Session and immediately writes its projection.

BindSession

Projects the selected Session path after successful saves; optional ProjectNow writes the loaded value once.

ClearCache / GetDiagnostics

Invalidates local sorted-query cache or reads request, retry, budget, cache and query metrics.

OrderedIndex.MemoryAdapter

Provides deterministic integer storage, sorted pages, failure injection and budget control for tests.

Owner clientVirtualStoreClient
How to use itRead-only nested path observer
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Client = require(
    ReplicatedStorage:WaitForChild("VirtualStoreClient")
)
local Replica = Client.connect("PlayerData")

Replica:Await()
Replica:Observe({"Wallet", "Coins"}, function(coins)
    coinsLabel.Text = tostring(coins)
end)

Client responsibility: display public data and send action requests. It never decides authoritative values or writes the replica.

Server responsibility: expose exact safe paths and validate every RemoteEvent request independently.

connect(name)

Connects to one named owner-only replica channel installed by the server.

Await / GetState

Waits for the initial snapshot or inspects whether replica data is ready.

Get / GetPath

Reads public replicated data only. The returned replica is read-only.

Observe(path, callback)

Runs a callback when one top-level or nested public path changes.

Changed / StateChanged

Signals general replica patches and connection-state transitions.

Disconnect

Stops observers and releases local replica resources during custom teardown.

Server signalsObserve lifecycle events
How to use itOperational monitoring and changed paths
Data.Store.CriticalStateChanged:Connect(function(critical, diagnostics)
    PurchaseService.SetEnabled(not critical)
    warn("Recent issues", diagnostics.RecentIssueCount)
end)

session.OnChanged:Connect(function(change)
    print(change.Reason, change.Source, change.Paths)
end)

Use signals for: monitoring, presentation updates, diagnostics and coordinating services after confirmed lifecycle events.

Do not use signals as: a replacement for persisted receipts or a guaranteed cross-server event bus.

store.OnError

Receives structured Store errors for logging and operational monitoring.

CriticalStateChanged

Fires when recent issue thresholds enter or recover from critical state.

SessionOpened / SessionClosed

Reports local active-session lifecycle changes.

OperationQueued / Applied / Retried / Failed

Reports durable-work progress for diagnostics and workflow coordination.

session.OnChanged

Receives reason, source, timestamp, revision and always-included changed paths.

session.OnSaved / OnReleased / HandoffRequested

Reports confirmed persistence, ownership end and cross-server handoff hints.

Settings

Change these only for a reason.

The defaults suit a normal game. Before changing one, check which request budget, timeout, or record-size tradeoff it controls. Keep LeaseTimeout greater than twice AutoSaveInterval.

Persistence timingRoutine write pressure
AutoSaveInterval = 60

Target seconds between routine session saves. Increase to reduce writes; decrease only after measuring budget pressure.

AutoSaveCheckInterval = 1

How often the scheduler checks for due sessions. It does not mean every session writes each second.

AutoSaveConcurrency = 2

Maximum simultaneous autosaves. Protects budgets during join spikes and large servers.

AutoSaveRetryInterval = 10

How quickly failed autosaves become due again.

LeaseTimeout = 180

How long ownership survives without renewal. Must exceed two autosave intervals.

Requests and shutdownBound waiting and retries
OpenTimeout = 20

Maximum time an open waits for an active lease or storage progress before returning an error.

RetryAttempts = 5

General bounded adapter request attempts before the operation returns failure.

BudgetTimeout = 10

Maximum wait for the correct Roblox request budget.

BudgetPollInterval = 0.25

Budget availability check frequency while waiting.

CloseTimeout = 25

Total bounded shutdown window for retrying final releases.

CloseRetryInterval = 1

Delay between failed shutdown release attempts.

CloseConcurrency = 10

Maximum simultaneous shutdown releases.

Operation ledgersDurability versus record size
OperationProcessLimit = 50

Maximum pending operations processed in one pass so one profile cannot monopolize a write.

MaxOperationAttempts = 10

Retry ceiling before pending work becomes a terminal Failed receipt.

OperationTTL = 0

Maximum pending age in seconds. Zero disables expiry.

UnknownOperationPolicy = "Retry"

Controls whether missing handlers retry, cancel or immediately fail.

OperationReceiptLimit = 500

Retained full result/error receipts. Larger values improve inspection but consume record bytes.

OperationDedupeLimit = 5000

Retained minimal completed-ID tombstones after full receipt pruning.

DeadLetterLimit = 100

Retained terminal failure details. Use lower values when operation payloads are large.

DeadLetterPayloadMaxBytes = 8192

Maximum payload bytes copied into one dead letter; larger payloads are omitted but measured.

WriteReceiptLimit = 20

Recent commit-token receipts retained for ambiguous save-response recovery.

Safety and observabilityValidation, audit and incidents
MaxDataBytes = 3800000

Rejects records before approaching Roblox's storage limit.

StrictData = false

Makes public Session data read-only when enabled. The player facade enables it by default.

TrackChangeDetails = false

Includes old/new detail in change events and diffs at additional memory cost.

AuditLogLimit = 0

Retains recent local audit entries. Zero disables the audit buffer.

CriticalErrorThreshold = 5

Recent issue count required to enter critical state.

CriticalWindow = 120

Seconds used when counting recent issues.

CriticalRecovery = 60

Healthy time required before leaving critical state.

EnableMessaging = true

Enables optional handoff and operation notifications. It is never the correctness mechanism.

AutoReleaseOnHandoff = true

Allows a current owner to release sooner after receiving a waiting-server hint.

OrderedIndex settingsSorted query cost and freshness
RetryAttempts = 5

Bounded OrderedDataStore request attempts before returning an explicit error.

RetryDelay = 0.5 / RetryBackoff = 2

Delay and exponential multiplier between failed projection or query attempts.

BudgetTimeout = 10

Maximum wait for OrderedRead, OrderedWrite, OrderedRemove or GetSortedAsync request budget.

DefaultPageSize = 50

Sorted page size used when the query does not specify one; Roblox permits at most 100.

MaxQueryEntries = 1000

Hard ceiling for one top query or rank scan request.

MaxRankScan = 1000

Default number of sorted entries scanned while searching for one key's rank.

CacheTTL = 0

Local sorted-query cache duration. Zero disables cache; writes clear this server's cached lists.

Errors and states

What to do when a call does not succeed.

VirtualStore returns an error instead of quietly opening blank data. Check the error code, log the detail, and stop the current gameplay action when the profile is not safe to use.

Player load stateFacade lifecycle
Loading

Initial open has started; gameplay must wait.

Retrying

A temporary load attempt failed and the same claim is retrying.

Ready

Validated active Session exists and gameplay may proceed.

Failed

Configured attempts ended without a safe profile; fail closed.

Releasing / Released

Final ownership end is in progress or confirmed.

Operation statusDurable work lifecycle
Queued / Pending

Work is stored but has not reached a terminal receipt.

Applied

Handler mutation and success receipt committed together.

Cancelled

Work deliberately ended without applying its intended mutation.

Failed

Work ended after policy, attempts, expiry or administrator action.

Unknown

No retained pending, receipt or dedupe record currently identifies the ID.

Common errorsRecommended response
LockTimeout

Another active lease did not release in time. Wait, retry later or investigate before exact-token repair.

LeaseLost

This server no longer owns the stored token. Stop writes and end gameplay tied to this Session.

PlayerDataUnavailable

Player facade has no Ready Session. Do not grant or mutate; wait for Ready or stop the action.

AdminAccessDisabled / Denied

Admin authorization is absent or rejected. Never work around it through a client remote.

LeaseTokenMismatch

The inspected lease changed before force release. Reinspect; never release a guessed token.

Before releaseChecks for games with valuable player data
  • Enable strict player profiles and perform mutations through helpers or transactions.
  • Use globally unique operation, receipt, purchase and trade IDs that are never recycled.
  • Keep random selection and external side effects outside retryable operation handlers.
  • Replicate exact public paths; audit every parent path and transform for private data leaks.
  • Use checkpoints or durable operations before presenting critical external flows as final.
  • Build persistent saga/journal recovery for trades, auctions and other multi-key workflows.
  • Keep admin capabilities server-only and monitor dead letters, critical state and diagnostics.
  • Test outages, lease conflict, duplicate retries, shutdown, migration and missing handlers before release.
  • Keep leaderboard projections separate from authoritative currency and expect temporary OrderedIndex lag.

Testing

Test the failure cases before publishing.

The included benchmark opens 100 memory-backed sessions and adds fake write latency. It is useful for checking scheduler behavior, but it is not a measurement of Roblox's live DataStore speed.

100simulated sessions
boundedautosave concurrency
visiblerequests, retries and latency

VirtualStore v0.1.0

Start with the simple API.

Use the player facade for normal data. Add durable operations, repair tools, and trade recovery only when the game actually needs them.