Roblox Matchmaking: MemoryStore Queues + Reserved Servers

Build Roblox matchmaking with MemoryStore and TeleportAsync reserved servers. Covers cross-server handoff, queue ownership, MMR, retries and recovery.

For a session-based Roblox experience, the native stack is already strong: MemoryStore holds short-lived queue or assignment state, MessagingService can wake other live servers, and TeleportService sends the matched players into a public or reserved destination.

The difficult part is not calling TeleportAsync(). It is designing ownership so that two lobby servers do not match the same player, a failed teleport does not lose the ticket, and players distributed across different lobby servers still converge on the same reserved server.

Native-first rule: keep ephemeral queue and teleport coordination inside Roblox unless you have a concrete cross-experience or external-operations reason not to. Durable MMR, progression, rewards and support/audit data are different concerns.

The native matchmaking flow

Lobby server A ─┐
                ├─► MemoryStore queue / rating buckets ─► match coordinator
Lobby server B ─┘                                      │
                                                       ├─ reserve destination
                                                       ├─ write shared assignment
                                                       └─ notify source lobbies
                                                                  │
                                              each source lobby teleports
                                              only its local Player objects
                                                                  │
                                                                  ▼
                                                       same reserved server
PrimitiveUse it forDo not use it for
MemoryStoreQueueShort-lived FIFO/priority tickets and work claimingPermanent MMR, inventory or match history
MemoryStoreSortedMapOrdered rating/time indexes when range selection mattersLarge durable player profiles
MemoryStoreHashMapAssignment state such as ticket → match/reservationLong-term progression
MessagingServiceBest-effort cross-server wake-up/notificationThe only copy of an assignment
TeleportServiceServer-side movement into the chosen destinationClient-trusted matchmaking decisions

1. Put small, expiring tickets in MemoryStore

Roblox describes MemoryStore as high-throughput, low-latency shared memory for rapidly changing data, and explicitly lists skill-based matchmaking as a use case. The queue documentation also gives you the key processing primitive: a read makes items temporarily invisible, and they become visible again if you fail to remove them before the invisibility timeout.

That gives you an at-least-once work pattern. Make the ticket itself idempotent:

local MemoryStoreService = game:GetService("MemoryStoreService")
local HttpService = game:GetService("HttpService")

local queue = MemoryStoreService:GetQueue("ranked-eu-v1", 30)

local function enqueuePlayer(player, mmr)
    local ticket = {
        ticketId = HttpService:GenerateGUID(false),
        userId = player.UserId,
        sourceJobId = game.JobId,
        mmr = mmr,
        queuedAt = os.time(),
    }

    -- Short TTL: expiration is a safety net, not long-term storage.
    queue:AddAsync(ticket, 120, 0)
    return ticket.ticketId
end

Do not put an entire player profile into the queue. Keep tickets compact and write persistent rating/progression to DataStore or your external persistent backend.

2. Queue vs sorted map for skill matching

A single queue is excellent when your rule is “oldest compatible party first” or when you have coarse buckets such as bronze-eu-duo. Priority queues can also influence processing order.

If your rule is “find players within ±100 MMR, then widen the window every ten seconds,” a sorted map can be easier because rating/time are naturally ordered. Another simple option is multiple queues by region/mode/rating band and periodically expand which neighboring bands a worker reads.

The important thing is to make the matching rule explicit. A priority value alone is not a complete skill-based matcher.

3. Claim work before you create the destination

MemoryStoreQueue:ReadAsync() returns the items plus an identifier used by RemoveAsync(). During the queue's invisibility timeout, another worker should not see the same claimed items. If your worker crashes before removal, the items eventually become visible again.

local function tryReadMatch(queue, teamSize)
    local items, readId = queue:ReadAsync(teamSize * 2, true, 2)
    if #items ~= teamSize * 2 then
        return nil
    end

    return {
        tickets = items,
        readId = readId,
    }
end

For production, validate that every ticket is still eligible: the user has not cancelled, the ticket is not already assigned, party membership is unchanged, and your mode/rating rules still match. Assume a queue item can be delivered more than once over its lifetime.

4. Reserve one destination for the whole match

You have two current TeleportService shapes for a fresh reserved destination:

  • set TeleportOptions.ShouldReserveServer = true when calling TeleportAsync(); or
  • call ReserveServerAsync(placeId), keep its access code, then use that code through TeleportOptions.ReservedServerAccessCode.

The second form is useful when multiple source lobby servers need to send their local players to the same destination, because they can all receive the same reserved-server access code.

local TeleportService = game:GetService("TeleportService")

local accessCode, privateServerId = TeleportService:ReserveServerAsync(MATCH_PLACE_ID)

local assignment = {
    matchId = matchId,
    placeId = MATCH_PLACE_ID,
    accessCode = accessCode,
    privateServerId = privateServerId,
    expiresAt = os.time() + 120,
}

Roblox documents that the access code remains valid indefinitely, that a server starts when the code is first used, and that PrivateServerId is stable across server instances created from that access code while JobId is not. Treat the access code as sensitive session-routing material even though its platform lifetime is long.

5. The cross-server handoff is the part most tutorials skip

A matchmaking worker can read tickets from every server in the experience, but a Roblox Player instance exists only in the game server that currently owns that player. If Alice is in lobby A and Bob is in lobby B, lobby A cannot pass Bob's remote Player instance to TeleportAsync().

Use shared assignment state plus a wake-up signal:

  1. write ticketId → assignment (or userId → assignment) into a short-lived MemoryStore hash map;
  2. publish a small MessagingService notification containing only the ticket/user identifier;
  3. each source lobby receives the signal, or discovers the assignment by polling, and looks up the shared assignment;
  4. that source lobby calls TeleportAsync() only for the local players it owns.

Do not make MessagingService the database. Roblox documents delivery as best effort and not guaranteed. The message should mean “check shared state now,” not carry the only copy of the match assignment.

6. Teleport from server code and retry the failures that can recover

Roblox's current teleport guide says TeleportAsync() should be called from server scripts for secure teleports, wrapped in pcall(), and paired with TeleportInitFailed because a teleport can fail even after the initial call succeeds.

local TeleportService = game:GetService("TeleportService")

local function teleportToAssignment(players, assignment)
    local options = Instance.new("TeleportOptions")
    options.ReservedServerAccessCode = assignment.accessCode
    options:SetTeleportData({
        matchId = assignment.matchId, -- routing hint only
    })

    local ok, result = pcall(function()
        return TeleportService:TeleportAsync(assignment.placeId, players, options)
    end)

    if not ok then
        warn("TeleportAsync failed", result)
    end
    return ok, result
end

TeleportService.TeleportInitFailed:Connect(function(player, result, message, placeId, options)
    -- Retry only according to your policy/result type.
    -- The event supplies options targeting the same destination.
    warn("Teleport failed for", player.UserId, result, message)
end)

Roblox also warns that teleport data is visible to the client and unencrypted. Put a match identifier or non-secret routing hint there, not currency, inventory truth, server credentials or an authorization decision.

7. Do not delete matchmaking state too early

The dangerous sequence is:

read queue → remove queue item → attempt teleport → teleport fails → player is lost

Instead, create the assignment first, make the handoff retryable, and only retire the queue claim when your state machine can recover the player from the assignment. A useful lifecycle is:

QUEUED → CLAIMED → ASSIGNED → TELEPORTING → ARRIVED
                    │              │
                    └── timeout ───┴──► requeue / cancel

The exact acknowledgement mechanism is yours: the destination can mark each expected user as arrived in MemoryStore, or your persistent backend can record session admission. The key property is that “RemoveAsync succeeded” is not treated as proof that a player reached the destination.

8. Current MemoryStore limits change the sharding decision

As of September 2026, Roblox documents MemoryStore quotas at the experience level, including a request-unit budget of 1000 + 120 × concurrent users per minute and memory that grows with concurrent users. A single queue or sorted map is also a single-partition structure and has its own size/item limits.

That does not mean “one queue is bad.” It means you should shard when your workload requires it, commonly by mode/region/rating band, and watch the Memory Store observability dashboard instead of inventing a fixed global queue count in advance.

9. Reserved-server details worth designing around

  • Studio limitation: reserved-server teleport behavior must be tested in a published experience / Roblox app, not ordinary Studio playtesting.
  • Stable private identity: PrivateServerId is associated with the reservation; JobId changes across server instances.
  • Cross-play caveat: Roblox documents that console players with cross-play disabled can arrive in a different server despite the same PrivateServerId; DataModel.MatchmakingType can distinguish those instances.
  • TeleportOptions are mutually exclusive: do not combine ReservedServerAccessCode, ShouldReserveServer and ServerInstanceId in incompatible combinations.

10. Where a persistent backend actually helps

None of the native machinery above requires Crux. Add a persistent backend only where the requirement is genuinely outside the ephemeral Roblox queue:

  • durable MMR / rank history and season state;
  • trusted post-match reward/progression writes;
  • cross-experience identity or progression that you intentionally want outside Roblox's own persistence boundary;
  • operator/support tools that run outside the live Roblox servers;
  • auditable match/session history and live configuration shared with non-Roblox services.

Crux's Roblox server-token SDK can cover persistent documents, leaderboards, economy, server registry and live config from trusted server scripts. Its current player-token matchmaking queue is not a replacement for this native server-token MemoryStore flow, so do not ship a Crux server token to a LocalScript just to call that queue.

Implementation checklist

  • Are tickets short-lived and idempotent?
  • Can a player cancel without being matched from a stale queue item?
  • Can two workers process the same logical ticket without creating two matches?
  • Is the reservation/assignment stored outside best-effort MessagingService?
  • Does each source lobby teleport only players local to that server?
  • Do you handle both synchronous TeleportAsync errors and TeleportInitFailed?
  • Can a failed handoff requeue/cancel instead of losing the player?
  • Is secure state excluded from TeleportData?
  • Have you tested reserved teleports in a published experience?
  • Are MemoryStore request/memory quotas observable before launch?

Related reading

Sources

Technical, pricing, and product claims were checked against these primary sources on the verification date above.