Tenant server pattern
A reference sketch, not the only valid shape — the pattern every real tenant tracker built and tested this way converged on: one persistent process, ticking on a fixed interval, assembling and starting matches server-side.
setInterval(async () => {
if (tickInFlight) return; // guard against overlapping ticks
tickInFlight = true;
try {
const waiting = getWaitingPlayers(); // your own queue/lobby
if (waiting.length < MIN_PLAYERS) return;
const group = waiting.splice(0, GROUP_SIZE);
const { ticketId } = await post('/matchmaking/hosted/ticket?game=checkers');
for (const player of group) {
await post(`/matchmaking/hosted/ticket/${ticketId}/players?game=checkers`,
{ playerId: player.id });
}
await post(`/matchmaking/hosted/ticket/${ticketId}/promote?game=checkers`);
const { group: promoted } = await getLastPromotedGroup();
await post('/matchmaking/hosted/start-match?game=checkers',
{ groupIds: [promoted.id] });
} finally {
tickInFlight = false;
}
}, 3000);Partition by whatever varies
A team-size game (2v2 vs 3v3) or a variable-player-count game (2, 3, or 4-player) needs one tracker partitioned by that dimension — a 2v2 lobby and a 3v3 lobby should never merge into the same ticket. Keep a separate waiting list per size, not one list with a size filter applied late.
Reentrancy is the real bug class here, confirmed directly by testing tenant trackers this session — a slow request from one interval tick overlapping the next produces duplicate ticket submissions. The
tickInFlight guard above isn't decorative.