Limits
Every cap Ledger enforces and where it comes from.
Some of these are Roblox's and some are Ledger's. The Roblox ones you can't move. The Ledger ones are set below the real limit so you get a clean refusal instead of a failed save.
Names and keys
| Cap | ||
|---|---|---|
| Store name | 47 characters | Ledger, so <Name>_Tx fits in the datastore's 50 |
| Entity key | 50 characters | Roblox |
| Transfer id | 64 characters | Ledger |
| Transaction id | 50 characters | Ledger |
Player keys are the UserId, so there's nothing to think about there. Entity keys have to be valid UTF-8.
Size
| Cap | ||
|---|---|---|
| Stored value | 4 MB | Roblox |
| Folded state | 2 MB | Ledger |
| One op | 1 MB | Ledger |
| Unsaved ops | 1.5 MB | Ledger |
| Queued ops | 4096 | Ledger |
State can hold numbers, strings, booleans, tables and buffers. A buffer costs about a third more than its length. That is what the datastore charges to store one, and what these caps count.
State caps at 2 MB rather than 4, which leaves room for the ops sitting on top of the snapshot.
An op that would push state over the cap is refused with Full. Ops that
shrink it are still allowed, so a cleanup works when you're already over.
Hitting the unsaved caps gives you Backlog, and in practice that only
happens when the datastore is down.
Ledger keeps three tables of its own in the state, and they clear on different rules:
| Holds | Clears after | |
|---|---|---|
_Received | names already applied | 30 days |
_Held | money set aside for a transfer | 7 days to deliver, 8 to give back |
_Booked | units set aside on the key | the reservation's hold, 15 minutes by default |
None of them grows with uptime. Each is bounded by how much happened inside its own window.
Applied names
The set of applied ids lives in the state, so it counts against the state cap. It does not grow without bound. Every time a name is written into it, anything already in there older than 30 days is dropped, whichever kind of name it is.
That sweep only rebuilds the set when something in it is actually old enough to go, so the usual write costs nothing.
One entry is about 40 bytes when Ledger picked the id, more if you named the transfer yourself, so a
rolling 30 day window is nowhere near the state cap for any real player. An entry also carries a short
fingerprint of what the name meant, which is what lets a name reused for a different amount answer
Spent instead of true.
ClearDelivered runs that sweep early. It cannot drop a delivery id sooner than 30 days, because a
sender can ask for a refund up to that point and needs the evidence to know the delivery already
happened.
Naming your transfers tip:8412 rather than after a random string keeps this set small.
Transactions
Between 2 and 4 legs, and a key can only appear once.
One key doesn't need a transaction, which is why two is the minimum. Edit already applies to a
single key or refuses it.
The maximum is four because each leg holds up a key while it waits. A leg is written to its key
first and sits there unresolved, and anything else writing to that key meanwhile answers
Busy. If the server running the transaction dies, another one has to
abort it, and it may not do that until 30 seconds per leg have passed. Four legs can hold four keys
for two minutes.
A transaction is considered dead after 60 seconds, so another server will abort it on the original's behalf. Committed markers get tidied up in the background about an hour later.
A transaction id ages out after 30 days, the same as every other name. A transaction lives at most an hour, so 30 days covers the whole protocol many times over. What it does not cover is you reusing the same id a month later, which applies it again. Derive ids from the thing being settled and they are never reused.
One key at a time
A key takes one transaction at a time. While a leg is parked on it, every other transaction touching
that key answers Busy and stops. Tx does not retry a Busy for you,
because every server retrying at once would multiply the load on a key that is already contended.
Retry it yourself with a short backoff. Measured against the mock, 8 servers depositing into one bank key all get through in about 3 attempts each. 32 servers take about 8 attempts each, and the cost per successful transaction goes from 10 requests to over 100.
Plan around this for anything every player writes to, so a guild bank, a global shop, or an event pot. Spread the writes across keys where you can, for example one key per guild rather than one for all of them. Correctness never suffers from contention, only throughput.
What an operation costs
Measured on the mock, for the path where nothing fails.
| operation | datastore requests |
|---|---|
Peek, Edit, Bump | 1 |
Reserve, Confirm, Release | 1 |
Grant | 4 |
Transfer | 3 |
Tx, 2 legs | 8 |
Tx, 4 legs | 12 |
Total | 16 |
A transaction costs 4 requests on its marker whatever the leg count, which is why the two leg case is the one worth avoiding. A limit that lives on one key is a reservation, not a transaction. See Reservations and totals.
Roblox gives an experience UpdateAsync budget of 300 + 20 per CCU a minute, which works out at
about 20 writes per player per minute at any real player count. So a player can average roughly 6
transfers or 2 two leg transactions a minute across the whole experience, before autosave takes its
share. Autosave itself is cheap and does not scale with how busy a player is, see
Timing.
Transfers
A stranded transfer is redriven for 7 days. After that it expires and refunds. Delivered ids stay in the receiver's applied set for 30 days so a late retry can't pay twice.
Recovery handles up to 32 holds per pass, so a key with a lot stranded takes a few passes.
Reservations and totals
A reservation lasts 15 minutes by default, not the 30 days an applied name lasts. Hold sets the time
per reservation, so a slow checkout can ask for longer without making every reservation wait.
Two things clear the ones that ran out. The next Reserve on that key clears every one of them at
once. The recovery sweep clears 32 a pass, so a key nobody reserves again takes a few minutes to drain.
That means only the reservations open right now take up room. One is about 55 bytes, so the state cap is about 37,000 open at the same time on one key. A 15 minute window makes that hard to reach.
The hold lives on the reservation, not in the store options. Every server then folds the same log the same way, whatever it is configured with.
The recovery sweep settles up to 32 overdue reservations a pass on one key. A key with a lot of them
takes a few passes. Each one that names a To costs a read of that key, because one already handed
over has to be settled rather than given back.
A total is spread over 16 keys. Total reads all 16, so it costs 16 requests. Bump costs one.
Timing
Autosave runs every 30 seconds per session, and compacts as well when the log has gotten long.
A session with nothing queued and no transaction parked on it reads every fourth turn. An idle player then costs one request every two minutes rather than one every 30 seconds. It writes as soon as there is anything to write. Only the check for what other servers did waits.
An autosave is skipped when the server is under 4 requests of write budget, and it warns.
Compaction
The log is folded into the snapshot once it is worth rewriting the record. The snapshot size decides when. A small profile compacts every 128 ops. A large one waits until the log is a fifth of the snapshot. Nothing waits past 512 ops.
A big profile is expensive to rewrite, so rewriting it every 128 ops costs more than carrying them. A small one is cheap to rewrite, and folding fewer ops keeps loads fast.
Versions
Roblox keeps 30 days of history per key. History pages up to 100 at a time and defaults to 25.
What isn't limited
There's no cap on how many servers write the same key at once. The fold is why. It's the one number you don't have to plan around.