Skip to content

Integrating PlayFab Matchmaking

import { Aside } from ‘@astrojs/starlight/components’;

PlayFab Matchmaking and PlayFab Multiplayer Servers are separate products. You can use PlayFab’s matchmaking queue without using PlayFab’s hosting. When a match is found, a backend bridge service calls the Gameye Session API to allocate a server, then delivers the IP and port to players through your own channel.

[Game clients] → latency ping → [Gameye /available-location]
[Game clients] → CreateMatchmakingTicket → [PlayFab Matchmaking]
[PlayFab] → ticket status: Matched
[Backend bridge] → polls GetMatchmakingTicket OR receives PlayStream event
[Backend bridge] → POST /session → [Gameye Session API]
[Gameye] → server running (~0.5s)
[Backend bridge] → push IP + port → [Game clients]

There is no off-the-shelf PlayFab package equivalent to the Nakama Fleet Manager. This integration requires a backend service you operate — the bridge between PlayFab’s match result and Gameye’s allocation API.

  • PlayFab title with Matchmaking enabled
  • Gameye API token (request sandbox access)
  • Game server Docker image registered with Gameye (handled during onboarding)
  • A backend service (Node, Python, Go, or similar) with access to both the PlayFab and Gameye APIs

Step 1: Configure a PlayFab Matchmaking queue without auto-allocation

Section titled “Step 1: Configure a PlayFab Matchmaking queue without auto-allocation”

In the PlayFab Game Manager, create or update your matchmaking queue:

  • Set Server allocation enabled to false (or leave it unset)
  • Do not attach a Build ID — you are not using PlayFab Multiplayer Servers

Your queue definition in the PlayFab API:

{
"QueueName": "ranked-match",
"MinMatchSize": 2,
"MaxMatchSize": 10,
"ServerAllocationEnabled": false,
"MatchInitializationTimeoutInSeconds": 30
}

With ServerAllocationEnabled: false, PlayFab’s role ends when the match is formed — server allocation is yours to handle.

Step 2: Collect Gameye region latency in game clients

Section titled “Step 2: Collect Gameye region latency in game clients”

Before submitting a matchmaking ticket, clients ping the Gameye latency IPs and include the results as a player attribute. Your backend bridge uses these measurements to pick the optimal region when calling Gameye.

Fetch available locations and their latency IPs:

GET https://api.gameye.io/available-location/your-image-name
Authorization: Bearer YOUR_API_TOKEN

Response:

{
"locations": [
{ "location": "europe", "latencyIp": "185.x.x.x" },
{ "location": "us-east", "latencyIp": "104.x.x.x" },
{ "location": "asia-east", "latencyIp": "43.x.x.x" }
]
}

Clients UDP-ping each latencyIp and submit the measurements in the Attributes of their matchmaking ticket:

{
"Creator": { "Entity": { "Id": "...", "Type": "title_player_account" } },
"QueueName": "ranked-match",
"Attributes": {
"DataObject": {
"Latencies": {
"europe": 42,
"us-east": 98,
"asia-east": 180
}
}
}
}

The backend bridge watches for matched tickets and allocates a Gameye session. There are two approaches depending on your architecture:

Option A: Polling (simpler, no Azure dependency)

Section titled “Option A: Polling (simpler, no Azure dependency)”

Your backend polls GetMatchmakingTicket for each open ticket and acts when the status becomes Matched:

// Node.js pseudocode
async function watchTicket(titleId, entityToken, ticketId, queueName) {
while (true) {
const res = await playfab.post('/Match/GetMatchmakingTicket', {
TicketId: ticketId,
QueueName: queueName,
}, { 'X-EntityToken': entityToken });
const ticket = res.data.Ticket;
if (ticket.Status === 'Matched') {
const match = await playfab.post('/Match/GetMatch', {
MatchId: ticket.MatchId,
QueueName: queueName,
ReturnMemberAttributes: true,
}, { 'X-EntityToken': entityToken });
await allocateAndNotify(match.data.Match);
break;
}
if (ticket.Status === 'Cancelled') break;
await sleep(500); // poll every 500ms
}
}
async function allocateAndNotify(match) {
// Aggregate player latencies to pick the best Gameye region
const region = pickRegion(match.Members);
const session = await gameye.post('/session', {
location: region,
image: 'your-image-name',
version: '1.0.0',
labels: { matchId: match.MatchId },
ttl: 3600,
});
// Deliver IP and port to each player in the match
for (const member of match.Members) {
notifyPlayer(member.Entity.Id, {
host: session.host,
port: session.ports[0].host,
matchId: match.MatchId,
});
}
}

Option B: PlayStream event (event-driven, requires Azure Function)

Section titled “Option B: PlayStream event (event-driven, requires Azure Function)”

Configure a PlayFab PlayStream rule to trigger an Azure Function when a match is found:

  1. In PlayFab Game Manager → Automation → Rules, create a rule:

    • Trigger: PlayStream event com.playfab.matchmaking.match_found
    • Action: Azure Function webhook
  2. Your Azure Function receives the event and calls Gameye:

# Python Azure Function pseudocode
import json, os, requests
def main(req):
event = req.get_json()
match_id = event['EventData']['MatchId']
members = event['EventData']['Members']
region = pick_region(members) # aggregate latency attributes
session = requests.post(
f"{os.environ['GAMEYE_API_URL']}/session",
headers={"Authorization": f"Bearer {os.environ['GAMEYE_API_TOKEN']}"},
json={
"location": region,
"image": os.environ['GAMEYE_IMAGE'],
"version": os.environ['GAMEYE_IMAGE_VERSION'],
"labels": {"matchId": match_id},
"ttl": 3600,
}
).json()
# Push connection details to players
notify_players(members, session['host'], session['ports'][0]['host'])
return {"status": "allocated", "sessionId": session['id']}

Option B is event-driven and more efficient at scale, but adds an Azure Function dependency. Option A is simpler to operate and works without Azure infrastructure.

Step 4: Deliver connection details to players

Section titled “Step 4: Deliver connection details to players”

PlayFab Matchmaking does not push server connection details to game clients — your backend is responsible. Common patterns:

  • Polling your own backend: Client polls an endpoint keyed by matchId; bridge writes {host, port} when the session is ready; client stops polling when the key appears
  • WebSocket push: Backend has a persistent connection to each client and pushes the connection packet directly
  • PlayFab CloudScript: Store {matchId → host:port} in PlayFab title data and have clients read it after the ticket transitions to Matched

Pass the host IP and the host port number from the Gameye session response:

{
"id": "session-abc123",
"host": "185.x.x.x",
"ports": [{ "type": "udp", "container": 7777, "host": 34521 }]
}

Clients connect to 185.x.x.x:34521 — not 7777.

When the match ends, your game server process should exit cleanly. Gameye monitors the container and reclaims compute when the process exits — billing stops at that point.

To terminate a session early from your backend:

DELETE https://api.gameye.io/session/session-abc123
Authorization: Bearer YOUR_API_TOKEN

The ttl field in POST /session is a hard maximum lifetime in seconds and acts as a backstop if the server process fails to exit on its own.

Migrating from PlayFab Multiplayer Servers

Section titled “Migrating from PlayFab Multiplayer Servers”

If your game server binary currently integrates the PlayFab GSDK (calls to GSDK.start(), GSDK.readyForPlayers()), those calls need to be removed before the server will work on Gameye. The GSDK heartbeats to a PlayFab agent process that will not be present in a Gameye container.

The removal is mechanical:

  • Strip GSDK startup and heartbeat calls
  • Remove the PlayFab agent sidecar from any deployment config
  • Replace any GSDK config reads (getConfigSettings()) with environment variables passed in via the Gameye args or a startup script

Your game server binary otherwise runs unchanged — Gameye cares only that the process starts, listens on the expected UDP port, and exits cleanly when the match ends.

  • No native Fleet Manager interface — Unlike Nakama, PlayFab has no first-party “bring your own allocator” interface. The backend bridge is custom code your studio maintains.
  • No webhook equivalent for matchmaking — FlexMatch fires an SNS event on MatchmakingSucceeded. PlayFab does not have a direct equivalent; the polling approach adds latency and the PlayStream approach requires Azure Function infrastructure.
  • GSDK removal required for MPS migrations — Studios migrating from PlayFab Multiplayer Servers must strip GSDK calls from their server binary before deploying to Gameye.