Ledger
Concepts

Apply and Commit

One is instant and local, the other is durable before it answers.

Both of them write an op. The difference is when you find out it stuck.

ApplyCommit
Gives you(boolean, Reason?)Future<boolean, Reason?>
Yieldsnoyes, on :Wait()
Durable when it answersnoyes
Costs a datastore requestnoyes
Sees other servers' writesnoyes

Apply

Apply runs your reducer against live state, updates it, tells observers, and queues the op. It never touches the datastore. The op goes out on the next autosave, which runs every 30 seconds, or on the next Flush, Commit or Unload.

local Ok, Why = Session:Apply("SpendGold", { Amount = 25 })

Use it by default, for whatever the player is doing right now. Spending, picking things up, progression, stats.

The answer is local. It knows what this server has, and not what another server appended a second ago. Normally a player writes from one server only, so that's fine.

When the answer changes under you

Apply returns true straight away, and the op only reaches the log on the next save. If another server wrote to that key in between, the fold can refuse your op when it finally lands, after you already told the player it worked.

You only see it when two different ops compete for the same rule. The same op twice looks fine, since both servers reach the same number either way. Two different ops don't:

-- 100 gold. server A spends 80, server B spends 30, neither knows about the other
A:Apply("SpendGold", { Amount = 80 })   -- A shows 20
B:Apply("SpendGold", { Amount = 30 })   -- B shows 70

-- both ops reach the log. A's lands first, so B's would take the balance negative
-- and the reducer refuses it. B's player watches 70 become 20

Nothing is lost or double spent, and every server agrees once it settles. The player on B still saw a number that was never true.

Render from state, not from the return value. The correction then arrives on its own through Observe. Use Commit where a wrong number for a moment is worse than waiting.

Commit

Commit pushes everything queued, appends the op, waits for the datastore to take it, refolds from what came back, and only then answers.

local Ok, Why = Session:Commit("GrantReward", { Item = "Sword" }):Wait()
if Ok then
	GiveTheSword()
end

Use it when you're about to do something you can't take back. Handing out a purchase, calling a webhook, telling another service the thing happened. true means the op is in the log, your reducer took it, and every server will agree from here on.

The fold runs against the real record, so Commit sees other servers' writes. A stuck transaction leg on the key can still change the answer afterwards. When that is possible, Commit says Unresolved.

CommitOp

CommitOp takes an op table you built yourself, so you can put a Once name on it or reuse an id across a retry:

local Ok, Why = Session:CommitOp({
	Id = HttpService:GenerateGUID(false),
	Kind = "GrantReward",
	Item = "Sword",
	Once = `order:{OrderId}`,
}):Wait()

Id and Kind are required. Once is optional, and it makes the op apply at most one time on that key. See Once.

Which one

Apply for gameplay, Commit for side effects.

If the player wouldn't notice a rollback, Apply. If someone would file a ticket about it, Commit.

Flush

Session:Flush() pushes queued ops without adding one. Autosave calls it. You rarely need it yourself, but it's there for the moment before you do something risky:

Session:Flush():Wait()

Many ops, one request

A flush writes the whole queue in one request. The number of ops does not change this. Use it when one action of a player must change more than one part of their data.

callrequests
Session:Applynone
Session:Flushone, for the whole queue
Session:Commitone, or two when ops are already queued
Store:Editone, and it takes one op
Session:Apply("Coins", { Amount = 500 })
Session:Apply("Sword")
Session:Flush():Wait()

Two ops, and one request. Three Commit calls cost three requests. Each Commit is durable before it answers. A Commit also writes the queued ops first, if there are any.

The ops go in together. Each op is still folded on its own. If the reducer refuses one op, the other ops stay applied. Write one op when a grant must be all or nothing. Let the reducer make each change for that one op. One op cannot apply in part.

On this page