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 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 is not reduced 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.
In addition to the memory size, the API requests (shared for all MemoryStoreService API calls) have the following limit per minute:
1000 + 100 ⨉ [number of users]
Note that the rate of requests to any single queue or sorted map is limited to 100,000 requests per minute.
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, an error an error response will be sent back when your requests are being throttled.
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.
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.
To place an item at the front of the queue, set the priority higher than the current highest set priority; in this 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 will access the same queue. If necessary, specify a non-default read operation timeout (default is 30 seconds).
After you get a queue, call any of the following functions:
||Add a new item to the queue.|
||Read one or more items from the queue as a single operation.|
||Remove one or more items previously read from the queue.|
AddAsync() that access a store’s queue are network calls that may occasionally fail. As shown above, these calls should be wrapped in
pcall to catch and handle errors.
Reading and Removing Data
To read one or more items from the queue at once, call
ReadAsync(). When you are done processing items, immediately call
RemoveAsync() to delete them from the queue, passing the
ReadAsync() returns. This ensures that you will 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:
Network failures can occur that lead to inconsistent data or duplication. To account for such cases, the memory store makes an item invisible immediately after reading it. This allows you 30 seconds (or another value specified through
GetQueue() to modify and remove the item without concern that it will be read multiple times. If an unexpected failure occurs, a future ReadAsync call will read the item again after the invisible time expires.
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 will always be sorted when you retrieve the key-value pairs.
While keys have a size limit of 128 characters, there isn’t a size limit for values as long as they don’t go over the memory size limit.
1: Numerical order may not work as expected without padding; for example “99” will sort after “100”. If numerical order is desired, pad the numbers on the left as in “00100” and “00099”.↩
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 you will always modify the latest value before updating. The latency for
UpdateAsync() is similar to
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 will access the same sorted map.
After you get a sorted map, call any of the following functions:
||Add a new key or overwrite the value if the key already exists.|
||Read a particular key.|
||Read all existing keys or a specific range of them.|
||Retrieve the value of a key from a sorted map and update it via a callback function.|
||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.
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.
To get multiple keys from the sorted map as a single operation, call
GetRangeAsync() lists all existing keys, but you can set the lower and upper bound of the key range.
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 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.
To remove a key from the sorted map, call
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 will not access production data so that you can freely test new features. The quota you have for Studio will be 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.