Hi everyone, here is a modular approach to designing efficient, scalable matchmaking using
MemoryStoreService
. I wanted to build a matchmaking module but wasn’t sure where to start. This is my theoretical implementation, which I plan to develop into a full module.
Overview
This guide outlines the design principles, data flow, and theories for creating a custom matchmaking system using Roblox’s MemoryStore
. It is structured to support features such as:
- Region-based queues
- Party/group matchmaking
- Custom implementation of matchmaking functions
- Cross-server coordination
This system is built entirely on MemoryStore
, as traditional DataStore
usage is currently limited based on the number of players in an individual server. Most matchmaking systems rely on a central coordinator server to manage matchmaking logic across all servers, but this setup can quickly hit DataStore
call limits when scaling — for example, with 1,000 players, a small coordinator server wouldn’t be able to keep up.
In contrast, MemoryStore
supports high-frequency, cross-server access and can handle this load effectively. Memorystore are still affected by rate limits which are talked in terms of “units” (1000 + 100 * [number of concurrent users] request units per minute).
Note: Roblox is planning to lift
DataStore
server-based limits in 2026, moving to a model based on concurrent users across the entire experience.
Becoming the Matchmaker Coordinator Server
When a player joins a matchmaker on a server, the matchmaker on that server will begin attempting to claim the role of matchmaker coordinator — a server responsible for managing matchmaking logic - for that mode and that region, as long as there are players in the queue of that server’s matchmaker.
Matchmaker Coordinator Election Process
-
Every
REFRESH_TIME
seconds, a server’s matchmaker with players in queue will attempt to become the Matchmaker Coordinator by callingUpdateAsync
on aMemoryStoreHashMap
key specific to the matchmaker mode and region:MemoryStoreHashMap:UpdateAsync( key, transformFunction, expiration, )
-
key
:
This is the string identifier used to locate a specific entry in the hash map. In the context of matchmaking, this key might represent a specific queue or region (e.g.,"ranked-solo-silver-na"
). -
transformFunction
:
A function that receives the current value associated with the key and returns a new value to be stored. This function allows atomic updates—ensuring that the value is only changed if it hasn’t been modified by another server in the meantime. -
expiration
:
A number representing how many seconds the new value should persist in the store before it automatically expires. This acts as a safeguard—if a server crashes or fails to refresh its claim, the key will eventually become available again. In matchmaking scenarios, this prevents a server from permanently locking the matchmaker role. This should be controlled by a variable calledEXPIRATION_TIME
.
-
-
The
transformFunction
works as follows:-
If the key has no old value (i.e., no matchmaker exists yet):
Return a new table:
{ serverId = game.JobId, timestamp = os.time() }
-
If the key has an old value:
If
value.serverId == game.JobId
: this server is already the matchmaker → update the timestamp and return the tableIf
value.serverId ~= game.JobId
: another server is already the matchmaker → returnnil
to cancel the update
For example, if a server is trying to “claim” the matchmaker role, it might check whether the current value is expired or unset, and then return its own identifier if the role is available.
local function tryClaim(current) if not current or current.serverId == game.JobId then return { serverId = game.JobId, timestamp = os.time() } end return nil -- Don't update if the role is already taken end
-
-
Wrap the
UpdateAsync
call in apcall
to safely handle possible errors.- If
pcall
fails → skip for this round and it will try again after theREFRESH_TIME
- If
pcall
succeeds → check returned valueIf
serverId == currentServerId
: you are now the matchmaker → start matchmaking logicOtherwise: another server is being used for matchmaking, if you were the matchmaker up to that point stop, and wait for the refresh time to try again.
- If
Notes about Matchmaker Election Process
- UpdateAsync consules a minimum of 2 units per request. A matchmaker will consume
(60 / REFRESH_TIME) * 2
units per minutes for the election process (if you setREFRESH_TIME = 5
it will consume ~24 units/min) - Setting
EXPIRATION_TIME = 2 * REFRESH_TIME
gives a small buffer in case of slight delays or network hiccups. - This setup ensures that if the matchmaker server crashes or stops refreshing, another server can take over after the expiration time.
Matchmaker Election Process Flow
Adding Players/Parties to the Matchmaker and Running Matchmaking
While Messaging Service sounds like a great tool for managing events, it does not scall well due to the logic behind its rate limits based per server instead of experience (imagine if the matchmaker coordinator is a new server that only has one player, it would cause issues for the rest of the other servers):
- Messages sent per game server 600 + 240 * (number of players in this game server) per minute
Yes the default limits are pretty high and it is unlikely you will hit them, but you never know, better be safe than sorry.
Party Addition Flow
When a new party joins a matchmaker, the following steps occur:
- Write Party to SortMap: The player (or their party group) is written to a
MemoryStoreSortedMap
, using a timestamp as the sort key to maintain queue order.- The SortedMap name is the mode name of the matchmaker followed by the region, the key is the userId of the party leader.
- If writing fails, the player is removed from the matchmaker and warned.
local PartyData = { PartyLeaderId = PartyLeader.UserId, PlayerIds= {PlayerFromParty.UserId, PlayerFromParty.UserId}, StartQueueTime = os.time() }
- Set a Refresh Timeout: A heartbeat repeat function to check if a match has been found on a hashmap of the mode name+region by using the userId of the party’s leader as key. When found you can teleport the party and its users to the private server with the access code.
- Set a Delay Timeout: A heartbeat timeout is initiated, which start the removes party flow (see further below) if no match is found within a certain window.
Matchmaking Logic Flow
Once the matchmaker coordinator server is selected, the matchmaking logic begins running:
-
Fetch Players: Up to 200 party of players are fetched from the sorted map, ordered by the time they joined the queue.
local PartiesInQueue= { [1] = { -- this is the data of a party in the sortedmap PartyLeaderId = Party1Leader.UserId, -- key in the sortedmap, userId of party leader PlayerIds= {Player1FromParty1.UserId, Player2FromParty1.UserId}, -- userId of players in the party StartQueueTime = os.time() -- this is the sort key }, [2] = { PartyLeaderId = Party2Leader.UserId, PlayerIds= {Player1FromParty2.UserId, Player2FromParty2.UserId}, StartQueueTime = os.time() } }
-
Custom Lobby Grouping: This is where you would form your matchmaking logic and create lobbies of parties or create lobbies of group of parties, … The floor is yours, you should add a custom function to your matchmaker to which you give a list of parties in queue, and which would return to the matchmaker a list of lobbies to create with the different parties defined.
local Lobbies = { [1] = { -- this is the match data of the first lobby AccessCode = 123456789, -- an access code generated through TeleportService PartyUsed = {"Party1LeaderId", "Party2LeaderId"}, Teams = { Team1 = {"Player1FromParty1Id", "Player2FromParty1Id"}, Team2 = {"Player1FromParty2Id", "Player2FromParty2Id"}, } } }
-
Validate Lobbies with
UpdateAsync
: For each group formed, check the sort map again usingUpdateAsync
to ensure each party is still in the queue (not removed or matched elsewhere).- Once lobbies are created, loop through each parties inside of the lobby and check that they still have an oldvalue in the sortmap. Use UpdateAsync with an expiration value set to 0 so that entries are removed from the sortedmap after being added in a lobby.
If no oldvalue, means that the party left the matchmaking in the meantime. This makes the lobby created invalid, you should track the previously removed groups from that lobby and readd them in the sorted map.
If oldvalue no one is using this party and it is still in queue.
- Once lobbies are created, loop through each parties inside of the lobby and check that they still have an oldvalue in the sortmap. Use UpdateAsync with an expiration value set to 0 so that entries are removed from the sortedmap after being added in a lobby.
-
Mark Parties with Lobby Info: For each party in valid lobbies:
- Write a separate
MemoryStoreHashMap
of the mode name+region entry keyed by the userId of the party’s leader to indicate which lobby they belong to. This entry is being listened by the players to track when a lobby has been created. - Write a server-level hash entry keyed by the access code with the match/lobby metadata (lobby access code, settings, map, server status, etc.). This entry can be used to see if a player crashed, if the match of the server is still running and if the player should be teleported.
- Write a separate
Party Removal Flow
A party should only be removed from the matchmaker object and disconnect the listeners if it still has an active entry in the sorted map — this should be done using UpdateAsync
to ensure atomic validation with an expiration set to 0. If the entry is no longer present, it means the matchmaker coordinator has already consumed it and the party has been assigned to a lobby, with teleportation pending.
If all members of a party leave the server, make sure to remove the party from the matchmaker object to avoid memory leaks. However, if only the party leader leaves, the party object should remain temporarily to allow the other players to be teleported to the match in case they’ve already been assigned.
Handling Players in a Party
This system requires that all players of a party are located in one single server (this saves complexity but this could be improved)
Some best practices for managing players in a party:
- When a player leaves the game:
- Attempt to stop the party’s matchmaking queue before removing them from the party.
- If the match was already found, some data should be saved in the player’s profile:.
- the party they were in (the leader’s userId is the most important)
- If their party was in the matchmaking queue — the mode and region.
- When a player joins the base lobby:
- Check their saved data to determine if they were previously in queue.
- Validate against the
HashMap
orSortedMap
to see if a match was found and if they need to be teleported to an assigned lobby. - This should also be a condition to not be in any active other parties to join another party
Unit Cost Breakdown (per example)
MemoryStore has API unit limits, so optimizing how many units are used per matchmaking cycle is critical:
-
For 200 party of 1 player, and each party is located in one server (so across 200 servers), and the group function will create one lobby per party(so 200 lobbies in total):
- 24 * 200 → for each server to try to become the matchmaker coordinator
- 200 → for writing the party data in the sorted map
- 200 → for retrieving the first 200 items in the sorted map
- 200 * 2 → validating the lobbies
- 200 → hashmap LeaderId → matchData
- 200 → hashmap AccessCode → ServerData
- 200 * 12 → party refresh to get match data
- 8400 units/min (total allocated units by Roblox for 200 players: 21 000)
-
For 1 party of 1 player, and each party is located in one server, and the group function will create one lobby per party:
- 24 * 1→ for each server to try to become the matchmaker coordinator
- 1 → for writing the party data in the sorted map
- 1 → for retrieving the first 200 items in the sorted map
- 1 * 2 → validating the lobbies
- 1 → hashmap LeaderId → matchData
- 1 → hashmap AccessCode → ServerData
- 1 * 12 → party refresh to get match data
- 42 units/min (total allocated units by Roblox for 1 player: 1100)
-
For 5 party of 5 player, and each party is located in the same server, and the group function will create one lobby for all the parties:
- 24 * 1→ for each server to try to become the matchmaker coordinator
- 5 → for writing the party data in the sorted map
- 5 → for retrieving the first 200 items in the sorted map
- 5 * 2 → validating the lobbies
- 5 → hashmap LeaderId → matchData
- 1 → hashmap AccessCode → ServerData
- 5 * 12 → party refresh to get match data
- 110 units/min (total allocated units by Roblox for 5 players: 1500)
Last Notes
I finished writing this at 2 am, this is all in theory and wasn’t tested yet on a big numbers. Writing this big document also helped me in better defining and seeing some edge cases to take into account. Happy to discuss in the comments the area that could be improved or miconception that I might have gotten. I’ll also see if I can create a module to share on this page so hit the bell if you don’t want to miss it