Skip to content

MemoryDriver

MemoryDriver is cross-server state: round status, global boss health, live leaderboards, matchmaking queues — anything every server needs to see now and nobody needs to keep forever. It wraps MemoryStoreService with the defining decision that values are buffer-serialized: MemoryStore quotas charge by size and cap values at 32KB, so every byte of a value is a byte of quota, and packed buffers are dramatically smaller than the JSON tables most code writes there.

It completes the infrastructure trio: DataDriver owns storage, MessageDriver owns events, MemoryDriver owns state.

One driver wraps one map. Kind picks which MemoryStore primitive backs it:

  • "HashMap" (default) — unordered key-value, the general case.
  • "SortedMap" — ordered keys, unlocks getRange() for leaderboard-style reads.

Every value goes through a three-stage pipeline on the way in, and the reverse on the way out:

  1. Encode. With a Codec, the value packs through that Serde schema — smallest output, typed fields. Without one, Serde.encode packs it adaptively — still smaller than JSON, no schema required.
  2. Feature-detect buffer support. MemoryStore’s acceptance of raw buffer values is probed at runtime per store, never assumed. The first write attempts a raw buffer; only an error mentioning type proves buffers are unsupported — a throttle or timeout during the probe must not lock the driver into the fallback, because base64 costs ~33% size overhead forever after.
  3. Pack. Raw buffer if supported; otherwise a base64 string prefixed CK1: — a versioned marker so a plain string someone else wrote can never be misread as packed data.

On read, the same detection runs in reverse: buffers and CK1:-prefixed strings decode through the codec; anything else is a foreign value — written by something that isn’t a MemoryDriver — and is handed back untouched. You can point a driver at a map that other code already uses.

Every operation runs through a retry ladder (BackoffSeconds, default { 0.25, 0.5, 1 }, attempts = #BackoffSeconds + 1); a zero entry retries without yielding a frame. MemoryStore throttles by request and by size — the packing halves one axis, the ladder absorbs transient failures on the other.

Shared world state, mutated safely from any server:

local GlobalBoss = MemoryDriver.new({
Name = "GlobalBoss",
Codec = Serde.schema({ Health = "NumberU32", Phase = "NumberU8" }),
DefaultTtlSeconds = 300,
})
GlobalBoss:update("current", function(state)
state = state or { Health = 1000000, Phase = 1 }
state.Health -= Damage
return state -- return nil to abort the write
end)

update is the safe mutation primitive: the transform receives the decoded current value (or nil for a missing key), and its return is re-encoded and written through UpdateAsync. Returning nil aborts — nothing is written, which is how you express “only take this slot if nobody owns it”:

Coordinator:update("owner", function(current)
if current ~= nil then
return nil -- someone else already claimed it; abort, write nothing
end
return { JobId = game.JobId, At = os.time() }
end, 30)

A live cross-server leaderboard rides a SortedMap:

local Board = MemoryDriver.new({
Name = "WeeklyKills",
Kind = "SortedMap",
Codec = Serde.schema({ Kills = "NumberU32", Name = "String" }),
DefaultTtlSeconds = 7 * 24 * 3600,
})
Board:set(tostring(Player.UserId), { Kills = Kills, Name = Player.Name })
local Ok, Top = Board:getRange(Enum.SortDirection.Descending, 10)
if Ok then
for Rank, Entry in Top :: any do
print(`{Rank}. {Entry.value.Name} — {Entry.value.Kills}`)
end
end

For persistent, paged, cross-week leaderboards see Leaderboards — MemoryDriver is the live tier.

Field Default Description
Name — (required) The MemoryStore map name.
Kind "HashMap" "HashMap" or "SortedMap". Only SortedMap supports getRange.
Codec nil Serde schema codec. Without one, values pack adaptively via Serde.encode.
DefaultTtlSeconds 300 Expiry applied when a call passes no ttl. Every write refreshes the TTL.
BackoffSeconds { 0.25, 0.5, 1 } Retry ladder; attempts = length + 1.
Store nil Injected mock store for tests — the spec suite runs entirely against fakes.
ForceBase64 false Skip buffer detection and always pack as CK1: base64 strings.
Member Description
MemoryDriver.new(config) → MemoryDriver Wraps GetHashMap(Name) or GetSortedMap(Name) unless Store is injected.
driver:set(key, value, ttl?) → (ok, err?) Encode, pack, SetAsync. Runs the buffer-support probe on first use.
driver:get(key) → (ok, value, err?) GetAsync + decode. A missing key is true, nil. A stored value that fails to decode is an error, not nil.
driver:update(key, transform, ttl?) → (ok, newValue, err?) Atomic UpdateAsync over the decoded value. transform returning nil aborts with nothing written.
driver:remove(key) → (ok, err?) RemoveAsync, retried.
driver:getRange(direction?, count?) → (ok, { { key, value } }?, err?) SortedMap only. Defaults: Enum.SortDirection.Ascending, 100. Every entry is decoded; one corrupt entry fails the whole read with the offending key named.
  • 32KB per value, quota by size. The packing exists because of this. A schema codec is the strongest lever: the spec suite shows { Score = 1234, Streak = 7 } packing to 5 bytes against its ~25-byte JSON form, and schema output is always smaller than adaptive.
  • TTL is not optional. MemoryStore data expires; that is the point. Pick DefaultTtlSeconds to match the data’s real lifetime — state that must survive belongs in DataDriver.
  • update transforms may run more than once. UpdateAsync can retry the transform under contention. Keep transforms pure — compute from the passed value only, no side effects.
  • Decode failures are loud. get and getRange return (false, nil, err) for a value that fails to decode rather than silently handing you nil — a schema change without a plan for in-flight values shows up here. Foreign (non-driver) values are the exception: they pass through as-is.
  • The fallback is sticky per driver instance, not per store. Once a type error proves buffers unsupported, that instance stays on base64. ForceBase64 pins the behavior up front if you need determinism in tests.