Memory Stores

MemoryStoreService is a data service that provides fast in-memory storage that is accessible across servers. It offers two primitive data structures: queues and sorted maps.

In comparison to a data store, a memory store provides lower latency and higher throughput in exchange for reduced durability. It's a good option for any data that rapidly changes, such as:

  • Global leaderboards - Store and update user rankings on a shared leaderboard inside a map with key-value pairs.
  • Skill-based matchmaking queues - Save user information such as skill level in a shared queue among servers, and use lobby servers to run matchmaking periodically.
  • Auction houses - A global marketplace where users from all servers list and bid for available goods. Store marketplace data inside a map as key-value pairs.

Memory Store vs. Data Store

The relationship between a memory store and a data store is similar to a computer's memory and hard disk, respectively. While similar in function, the purpose of the data they store is very different. A memory store is for fast but non-persistent data access. It vanishes when data expires. For this reason, if you need data to be persistent, use a data store.

Limits

To maintain the scalability and system performance, memory stores have memory size quota, API requests limits, and data structure size limits.

Memory Size Quota

The memory quota limits the total amount of memory that an experience can consume. It's not a fixed value. Instead, it changes over time depending on the number of users in the experience according to the following formula: 64KB + 1KB ⨉ [number of users].

When users join the experience, the additional memory quota is available immediately. When users leave the experience, the quota doesn't reduce immediately. There's a grace period of 24 hours before the quota reevaluates to a lower value.

When an experience exceeds the memory quota, the operations that consume memory return an error. To avoid such errors, either explicitly delete unneeded items or rely on item expiration. Generally, explicit deletion is the preferred way of releasing memory as it takes effect immediately. Item expiration provides a safety mechanism to ensure that unused items do not remain in memory forever.

The memory size quota is applied on the experience level instead of the server level.

API Requests Limits

For API requests limits, there's a Request Unit quota applies for all MemoryStoreService API calls, which is 1000 + 100 * [number of concurrent users] request units per minute. Additionally, the rate of requests to any single queue or sorted map is limited to 100,000 requests per minute.

Most API calls only consume one request unit, with the exceptions of MemoryStoreService:GetRangeAsync() for sorted maps and MemoryStoreService:ReadAsync() for queues. These two methods consume units based on the number of returned items with at least one request unit. For example, if MemoryStoreService:GetRangeAsync() returns 10 items, the total quota counts based on 10 request units. If it returns an empty response without items, the quota counts based on a single request unit.

The requests quota is also applied on the experience level instead of the server level. This provides flexibility to allocate the requests among servers as long as the total request rate does not exceed the quota. If you exceed the quota, then you receive an error response when the service throttles your requests.

Data Structure Size Limits

For a single sorted map or queue, note the following size and item count limits:

  • Maximum number of items: 1,000,000

  • Maximum total size, including keys for sorted map: 100MB

If you need to store data that surpasses this limit for your experience, you can adopt the sharding technique to split and distribute them through key prefix into multiple data structures. Sharding memory stores can also help improve the scalability of your system.

Queues

A queue is a collection of items, such as strings, that are maintained in a first-in-first-out (FIFO) sequence. In a typical queue, you add items to the back of the queue while you read and remove items from the front of the queue.

How a regular queue adds, reads, and removes items

Priority of Items

While a queue defaults to FIFO order, there are situations where a queue needs to read certain items first regardless of the order in which they were added. In such cases, set a priority while adding an item. The queue reads an item with a high priority before an item with a low priority. Setting items' priorities is useful in matchmaking, where you can set a user to a higher priority if they have been waiting for a long time.

In the following image, a user is adding an item with a set priority of 3 to a queue. The item at the back of the queue has the default priority of 0 while the item at the front of the queue has the highest set priority of 5. The new item is placed behind all items with a set priority of 3.

An item's set priority changes the order in which a queue reads items

To place an item at the front of the queue, set the priority higher than the current highest set priority. In the previous example, the item needs a set priority of 6 or higher.

Getting a Queue

To get a queue, reference MemoryStoreService and then call GetQueue() with a name for the data structure. The name is global within the experience, so any place that uses the same name accesses the same queue. If necessary, specify a non-default read operation timeout (default is 30 seconds).


local MemoryStoreService = game:GetService("MemoryStoreService")
local queue = MemoryStoreService:GetQueue("Queue1")

After you get a queue, call any of the following functions:

Function Action
AddAsync() Add a new item to the queue.
ReadAsync() Read one or more items from the queue as a single operation.
RemoveAsync() Remove one or more items previously read from the queue.

Adding Data

To add a new item to the queue, call AddAsync(), providing the item, its priority, and an expiration time in seconds.


local MemoryStoreService = game:GetService("MemoryStoreService")
local queue = MemoryStoreService:GetQueue("Queue1")
local addSuccess, addError = pcall(function()
queue:AddAsync("User_1234", 0, 30)
end)
if not addSuccess then
warn(addError)
end

Reading and Removing Data

To read one or more items from the queue at once, call ReadAsync(). When you finish processing items, immediately call RemoveAsync() to delete them from the queue, passing the id that ReadAsync() returns. This ensures that you never process an item more than once.

To capture and respond to all items that are continuously being added to a queue, include a loop similar to the one within the following script:


local MemoryStoreService = game:GetService("MemoryStoreService")
local queue = MemoryStoreService:GetQueue("Queue1")
local addSuccess, addError = pcall(function()
queue:AddAsync("User_1234", 0, 30)
end)
if not addSuccess then
warn(addError)
end
-- Queue processing loop
while true do
local readSuccess, items, id = pcall(function()
return queue:ReadAsync(1, false, 30)
end)
if not readSuccess
wait(1)
elseif #items > 0 then
print(items, id)
local removeSuccess, removeError = pcall(function()
queue:RemoveAsync(id)
end)
if not removeSuccess then
warn(removeError)
end
end
end

Sorted Maps

A sorted map is a key-value data structure in which the keys are sorted in alphabetical order1. The order that you enter the keys in the map does not matter because the keys are always sorted when you retrieve the key-value pairs. Keys have a size limit of 128 characters, and there is a value size limit of 32KB.

Key Value
2021 cool
apple 34565
alpha hello
banana 456721
player_4634 876343

Concurrency of Keys

Multiple servers may update the same key at the same time, meaning other servers may change the value between the last read and update. In this case, use UpdateAsync() to read the latest value as the input for your callback function so that always modify the latest value before updating. The latency for UpdateAsync() is similar to GetAsync() plus SetAsync() unless there is contention. When contention occurs, the system automatically retries the operation until successful.

Getting or Creating a Sorted Map

To get a sorted map, reference MemoryStoreService, then call GetSortedMap() with a name for the data structure. The name is global within the experience, so any place that uses the same name access the same sorted map.


local MemoryStoreService = game:GetService("MemoryStoreService")
local sortedMap = MemoryStoreService:GetSortedMap("SortedMap1")

After you get a sorted map, call any of the following functions:

Function Action
SetAsync() Add a new key or overwrite the value if the key already exists.
GetAsync() Read a particular key.
GetRangeAsync() Read all existing keys or a specific range of them.
UpdateAsync() Retrieve the value of a key from a sorted map and update it via a callback function.
RemoveAsync() Removes a key from the sorted map.

Adding or Overwriting Data

To add a new key or to overwrite the value of a key in the sorted map, call SetAsync(), providing the key name, its value, and an expiration time in seconds. The maximum expiration time is 45 days (3,888,000 seconds).


local MemoryStoreService = game:GetService("MemoryStoreService")
local sortedMap = MemoryStoreService:GetSortedMap("SortedMap1")
local setSuccess, isNewKey = pcall(function()
return sortedMap:SetAsync("User_1234", 1000, 30)
end)
if setSuccess then
print(isNewKey)
end

Getting Data

To get one key from the sorted map, call GetAsync(). You need to set an expiration time for the added key so that the memory automatically cleans up once the key expires.


local MemoryStoreService = game:GetService("MemoryStoreService")
local sortedMap = MemoryStoreService:GetSortedMap("SortedMap1")
local setSuccess, isNewKey = pcall(function()
return = sortedMap:SetAsync("User_1234", 1000, 30)
end)
if setSuccess then
print(isNewKey)
end
local getSuccess, getError = pcall(function()
sortedMap:GetAsync("User_1234")
end)
if not getSuccess then
warn(getError)
end

To get multiple keys from the sorted map as a single operation, call GetRangeAsync(). By default, GetRangeAsync() lists all existing keys, but you can set the lower and upper bound of the key range.


local MemoryStoreService = game:GetService("MemoryStoreService")
local sortedMap = MemoryStoreService:GetSortedMap("SortedMap1")
-- Get up to 20 items starting from the beginning
local getSuccess, items = pcall(function()
return sortedMap:GetRangeAsync(Enum.SortDirection.Ascending, 20)
end)
if getSuccess then
for _, item in ipairs(items) do
print(item.key)
end
end

Updating Data

To retrieve the value of a key from a sorted map and update it, call UpdateAsync(), providing the key name, a callback function to update the key, and an expiration time in seconds. The maximum expiration time is 45 days (3,888,000 seconds).

The following example updates the highest bid for an auction item. UpdateAsync() ensures that the highest bid is not replaced with a lower value even if multiple servers update the same key simultaneously.


local MemoryStoreService = game:GetService("MemoryStoreService")
local sortedMap = MemoryStoreService:GetSortedMap("AuctionItems")
local function placeBid(itemKey, bidAmount)
local success, newHighBid = pcall(function()
return sortedMap:UpdateAsync(itemKey, function(item)
item = item or {highestBid = 0}
if item.highestBid < bidAmount then
item.highestBid = bidAmount
return item
end
return nil
end, 30)
end)
if success then
print(newHighBid)
end
end

Removing Data

To remove a key from the sorted map, call RemoveAsync().


local MemoryStoreService = game:GetService("MemoryStoreService")
local sortedMap = MemoryStoreService:GetSortedMap("SortedMap1")
local setSuccess, isNewKey = pcall(function()
return sortedMap:SetAsync("User_1234", 1000, 30)
end)
if setSuccess then
print(isNewKey)
end
local removeSuccess, removeError = pcall(function()
sortedMap:RemoveAsync("User_1234")
end)
if not removeSuccess then
warn(removeError)
end

To flush your memory in sorted maps, list all your keys with GetRangeAsync(), then remove them with RemoveAsync().


local MemoryStoreService = game:GetService("MemoryStoreService")
local sortedMap = MemoryStoreService:GetSortedMap("SortedMap1")
-- Initial lower bound of nil starts flush from first item
local exclusiveLowerBound = nil
while true do
-- Get up to a hundred items starting from current lower bound
local getRangeSuccess, items = pcall(function()
return sortedMap:GetRangeAsync(SortDirection.Ascending, 100, exclusiveLowerBound)
end
if getRangeSuccess then
local removeSuccess = true
local removeError = nil
for _, item in ipairs(items) do
removeSuccess, removeError = pcall(function()
sortedMap:RemoveAsync(item.key)
end)
end
-- If there was an error removing items, try again with the same exclusive lower bound
if not removeSuccess then
warn(removeError)
-- If range is less than a hundred items, end of map is reached
elseif #items < 100 then
break
else
-- The last retrieved key is the exclusive lower bound for the next iteration
exclusiveLowerBound = items[#items].key
end
end
end

Testing in Studio

To ensure that you can safely test a memory store before going to production, the MemoryStoreService offers separate namespaces for API calls from Studio versus those from runtime servers. As a result, your API calls from Studio don't access production data so that you can freely test new features. The quota you have for Studio is very limited, similar to the minimum quota of an experience.

Debugging Production Data

To debug a memory store on live experiences, use the Developer Console. Navigate to the "Log" tab, and click "Server". Find the "Command line" on the bottom and type the Lua code. Follow the same steps as you'd use in a Lua script to read and write the data.

Footnotes

  1. Numerical order may not work as expected without padding. For example, "99" sorts after "100". If you want a numerical order, pad the numbers on the left as in "00100" and "00099".