Who holds the queue
Holds the queue of those who want to play: it keeps every request until it finds an opponent. It checks the queue at a steady rhythm, with a maximum waiting time beyond which a fallback route kicks in.
Sigildream
Tech Blog
A player presses «Enter the Arena». At the same instant, thousands of kilometres away, someone else does exactly the same. A few seconds later they are face to face in the same live duel. In those few seconds an invisible match is played out: deciding who faces whom so the challenge is balanced, and where in the world to run the clash.
Act 1 · The four seconds
Behind that single tap on «Enter the Arena», three systems pass the baton like a relay. The first holds the queue: it collects the request to play and keeps it until it finds an opponent. The second is the pairing referee: it decides who faces whom so the challenge is balanced, and assigns an arena. The third lights up that arena — a dedicated server, in the corner of the world closest to both. They are separate roles for a good reason: this way each does one thing only, and when something slows down it is immediately clear which of the three to look at.
Holds the queue of those who want to play: it keeps every request until it finds an opponent. It checks the queue at a steady rhythm, with a maximum waiting time beyond which a fallback route kicks in.
A dedicated service, called with a signed internal call for every proposed pair. It compares the value of the two players, prepares the single-use match keys and picks the nearest region. Steady-state footprint: 12 MiB of RAM and 0.4% of CPU across more than 2,800 pairs.
Geolocated micro-instances light up a small arena in the region nearest the two players, host it and shut it down as soon as it ends. If one is already free, delivery is under a second; otherwise a new one is born in about 5 seconds.
Under the hood. They used to be a single block inside Nakama, our main server. Under strain — ten thousand players at once in the worst case — that block devoured memory and even slowed down other players logging in. We split it into three independent services, and the memory used dropped from 3.08 GiB to 1.59 GiB under the same load. Above, who they are and what they really cost.
a single Nakama block → three services · −1.5 GiB of RAM
Why separate them? The main server is excellent at tasks that are in no hurry: holding sessions, storing data, sending notifications. Acting as the pairing referee under heavy load, however, is not what it does best — and in our measurements it showed.
The stress test that convinced us to separate it tells of two advantages. The first is memory: from 3.08 GiB to 1.59 GiB, under the same workload. The second is the freedom to evolve the pairing rules independently, without waiting for updates to the main server.
A new rating rule, an extra defence against those who create a fresh account to beat beginners, another backup route to the match against the computer: that single service is updated and restarted in a few seconds. The main server, the database and the app on the phone remain untouched.
Act 1 · the flow, second by second
For the player it is just a wait of a few seconds. Behind that spinner the three roles pass the baton along a precise relay: collect the player, find the opponent, light up the arena, hand each of them the key to enter it. The line below shows that relay second by second, with the time each step can take.
Under the hood. The player's app sends a single request; everything else happens on our servers. Seven systems coordinate along a path with three signed hand-offs — so no one can impersonate anyone else.
The player’s app
The app opens a connection to the main server.
If it goes wrong: it retries, waiting a little longer each time; the button stays locked while the request is in progress, to avoid double taps.
The main server
The request enters the line.
If it goes wrong: once the maximum waiting time passes, the same request is promoted to a match against the AI.
The main server
A signed internal call to the pairing service.
If it goes wrong: if the service does not answer, the path falls back on its own to a backup route, with no errors for the player.
Pairing service
Rating computation and exclusive assignment of the match.
If it goes wrong: if the store is slow, it falls back to the backup route; if the match turns out to be already assigned, the pair is re-proposed.
Pairing service
A signed key for each player.
If it goes wrong: the keys are valid for a few minutes and are consumed on the first entry into the arena; a second attempt with the same key is rejected.
Geolocated micro-arenas
An arena lights up in the nearest region — or the local twin in development.
If it goes wrong: if a region is struggling, the allocation moves elsewhere without our code stepping in.
Player’s app → combat server
The app enters the arena with its match key.
If it goes wrong: the reconnection protocol covers server changes — see the chapter on combat.
Act 2 · Balanced opponents, not random
A random opponent is a coin toss: boring for the strong, punishing for the beginner. That is why pairing is not random. Before placing two people opposite each other the system asks two questions: how good each is, and how sure we are of it. A freshly registered player and a veteran returning after a month may have the same rating on paper, but we know far less about the first — and treating them as if we knew just as much would produce lopsided matches. Measuring our uncertainty too is what turns pairing into a real challenge rather than a roll of the dice.
Under the hood. A naive rating system keeps a single number per player: how good they are. Ours keeps three: how much we think they are worth (μ), how much uncertainty there is around that value (φ) and how volatile that value is over time (σ). All three matter, for instance, when pitting a veteran who has not played for a month against a second account with which a strong player hides among beginners, perhaps in the middle of a winning streak. On top of these three numbers our system adds four in-house corrections, each closing one of the four ways in which, in competitive turn-based games, people try to game the rating.
The 24 characters do not all win with the same ease: some, in the current stage of the game, are stronger and others weaker. The rating accounts for this starting advantage or disadvantage, so that whoever picks a weaker character in the current stage is not penalised twice. The comparison between characters is redone from time to time, in the background; while playing, the already-computed value is fetched in an instant, without slowing the match.
If a much stronger player continually faces people far weaker than themselves, they gain far less rating from their wins and lose far more from their losses. On top of that, accounts less than an hour old are kept in a separate line, so a wave of such profiles does not touch those who play fairly.
Several losses in a row raise our uncertainty (φ): it means we are no longer so sure of what that player is really worth. This cushions the losses that follow and avoids a runaway collapse. It is the least one can do not to drive people away: without it, one bad evening would turn into a 200-point drop to claw back over weeks.
If a player stays away for a long time, our confidence in their worth loosens, but only up to a certain limit — so a veteran returning after a month does not immediately find themselves up against one of the very best on their first Sunday back.
Act 2 · the knobs that keep the queue moving
Finding a balanced opponent costs time: the longer the search stays narrow, the better the pairing — but no one wants to stare forever at a spinning wheel. All the tuning in this part lives inside this tension: wait long enough to find the right person, widen the search before the wait becomes an annoyance, and keep the queue flowing even when thousands of requests arrive at the same instant.
Under the hood. Governing that rhythm is a handful of numbers on our main server — how often to check the queue, how long to keep a request alive, how many to hold at once. The default settings were simply too conservative for the worst case, tens of thousands of players queued at the same moment. The values below are the ones that keep the queue moving instead of piling up delay.
More often weighs too heavily on compute; less often overshoots the 3–5 s promised to players.
Beyond this threshold it switches to a match against the AI: the balance point between waiting and seeming stuck.
Twice the 10,000-player scenario, headroom for the «all at once» peak.
The factory setting jammed within a second under 10,000 players; these values held up in every stress test.
Act 2 · the referee, opened up
The piece that really decides who faces whom is a small service of its own: the referee. For every proposed pair it works out whether the clash will be balanced, assigns an arena and hands each player the key to enter it. It is fast and cheap to keep running, and in this part it is opened up piece by piece.
Under the hood. It is a small dedicated service, with a handful of functions and a state model pared to the bone. Beneath the surface, the «two promises» are two technical guarantees: that a match's identity can be spent only once, and that winner and loser stay bound to exactly the two paired players. Here are the service's surface and the two invariants.
Its surface is a handful of internal functions, not exposed to the public: the health checks for monitoring, the pairings requested by the main server, the matches against the AI and the rating update at the end of a match. They are all calls between our services, each one signed and verified.
The two operations — finding the pair and lighting them an arena — have been merged into a single step, to halve the time and never leave a match half-done: either everything happens, or nothing does.
The response delivers everything needed to enter the arena: where the fight happens, a single-use match key for each player, and a few numbers on the quality of the pairing (how close the two ratings are).
Act 3 · Cheat-proof
A phone is a screen, not a referee. It cannot be taken at its word when it declares a rating, claims a victory or states who it is. This is where the whole structure of this section begins: every message carries a signature, every match hands out single-use keys, and every result is re-verified on our servers before a single coin or ranking position moves. It starts with the signature — the way in which, even between our own servers, one proves it really is itself and not an impostor, like a wax seal that no one else can reproduce.
Under the hood. Every internal call to the pairing service is a signed request: the signature travels together with the message, and whoever receives it checks that it is authentic before heeding a single word. Here is the idea, without the recipe.
In practice, every internal message carries with it a cryptographic signature that certifies both its sender and its content together: if even a single character of the message changes, the signature no longer matches and the request is rejected. The signature is valid for a very short time and cannot be reused, so not even someone who managed to intercept it could replay it.
Act 3 · the match key
Once the pair is found, each of the two players receives a key — not just any key, but one that opens exactly one door, once only, and that demagnetises itself after a few minutes. It is the same idea as a hotel keycard: only the right guest enters the right room, and once the stay is over it is good for nothing. This way the two paired opponents — and no one else — end up in the same arena, and that key cannot be reused to sneak in a second time.
Under the hood. The keys are two signed tokens, one per seat, valid for a few minutes and with no renewal. What makes them secure, rather than a mere formality, is the way they are «consumed»: the first time one of the two enters the arena, the key is marked as spent in a single indivisible operation, and every subsequent attempt is rejected. Here is the idea.
Every key says the essentials and nothing more: who its owner is, which match it is valid for and with which character. The distinction between the two seats, and the opponent's identity, do not live inside the key. When a player shows up at the arena, the combat server verifies the signature and then «spends» the key in a single indivisible operation: from that moment it is spent, and anyone who re-presents the same key is rejected. It is exactly what prevents replaying the same match twice to double the reward.
Act 3 · the result, and the holes we closed
Once a match is over, the players cannot be taken at their word on the result: the verdict is reconstructed and re-verified on our servers before a single coin changes hands or a ranking position shifts. Only then does the same result become three things at once — the reward for the winner, the ranking update and a row of data with which we train the artificial intelligence that will one day play like a champion.
Under the hood. When a match ends, the combat server does not create the rewards itself: it never touches either the wallet or the ranking. It signs the verdict and sends it, with two separate signed messages, to the two services that have the authority to make it effective — one credits the coins and updates the ranking, the other updates the ratings. Each redoes its own checks before moving anything.
Only if all these checks pass — together with the positive outcome of a further layer of security, which stays confidential and serves to verify the flow of messages — is the result made effective.
The end-of-match verdict travels as a signed message, and whoever receives it does not trust it on faith: it checks that the signature is authentic, that it comes from an authorised combat server, that it is recent and that it has not been seen before. Only if all these checks pass is the result made effective.
Act 4 · Combat runs close to the players
A clash does not run in a single large compute centre on the other side of the planet: it runs in a small self-standing arena, born in the region nearest the two players, hosting the match and vanishing as soon as it ends. It is an advanced system of geolocated combat micro-instances — many small independent arenas that appear where they are needed, when they are needed. This way the duel is responsive and fair wherever the challengers are, in Milan as in Tokyo.
Under the hood. It is a distributed architecture that we build and run ourselves, on real production cloud infrastructure, with four concrete advantages.
The combat load is the most unpredictable part of our infrastructure: at peak hour the demand in Europe can be worth four times that of Asia in its quiet hours, within the same 24-hour span. A system of micro-arenas that light up and shut down on their own, region by region, follows this wave without keeping a peak-sized fleet running for nothing.
Close to the players. The arena lights up in the region nearest to both players, not in a single point of the globe: commands travel less distance and the duel stays responsive and fair even across different continents.
It does not stop. If a region runs into trouble, the clash is moved on its own to a nearby region, without anyone noticing and without losing the match in progress.
It grows and shrinks on its own. The number of arenas follows demand minute by minute across the world: thousands when everyone is playing, almost none in the empty hours, no capacity kept running for nothing.
Part of the system, not an accessory. It runs on real production cloud infrastructure and is woven deeply into the rest of the platform: the decision of who faces whom, the unfolding of the match and the final report always remain in our hands.
In each region a small group of arenas is kept on and ready; our system picks one and receives the address to send the two players to; the arena welcomes the challengers and hosts the match; once the match is over it signs the verdict, the rewards service credits the coins and updates the rankings, and the arena returns to the group of ready ones — the surplus ones shut down.
In development the same micro-arena system runs in a local twin: it speaks exactly the same language and lights up combat arenas when the main server is too loaded. Running the same code in development and in production means scale problems surface on the laptop, not on a Sunday evening. This twin too was put through a security audit before being used, and is confined to the smallest possible footprint.
Act 4 · the numbers, measured in the field
Every figure in this part comes from an empirical reading under load, not from an estimate. The goal is to state precisely what the stack holds up when pushed, and where it begins to crack — even when the answer is uncomfortable: only numbers anchored to load tests actually run, never a figure «in the millions» where the measurements say thousands.
Under the hood. Here are the measured limits, row by row — sustained load, worst case, gains after separating the pairing referee from the main server — with the source of every figure alongside and, where a number is a design goal rather than a measurement already taken, it is stated openly.
Every claim about capacity in this chapter is anchored to a load test recorded in our internal documents. The numbers below come from empirical readings under load, not from estimates.
Combat server ~60 MiB, main server ~3 GiB, all pairings between humans. Stable and already in use.
Baseline internal load test
The database and the pairing service jam instantly. The 23 April report records ~2,000 matches completed successfully with ~4,500 failed pairing attempts immediately retried; the steady-state split «8,000 queued + 2,000 started» cited in earlier drafts is a design estimate, not a measurement from that report.
Internal load report, 23-04-2026 (Phase A)
Main server memory 1.59 GiB against 3.08 GiB at the start (−48%). Pairing service 12 MiB and 0.4% of CPU. More than 2,800 pairs. Zero signature errors.
Internal load report, 24-04-2026 (Phase A′)
From 32 to 64 ready workers. The factory setting jammed on the burst of matches against local instances during the 10,000-player peak.
Main server configuration, after the audit
Direct writes, one at a time, today; the batched variant that holds much higher volumes is a goal for the next phase, not yet released.
Rating store
The tool that simulates load on a single Mac tops out around 2,800 fake players, because of the machine's limits. To reach 10,000 without hitting that ceiling we keep a second machine, with a copy of the project, that pushes further fake players toward the Mac over the local network. That is how we really verified the «10,000-player» scenario: every 10,000 figure in the tables above comes from a test driven by that second machine.
Act 4 · when the queue stalls
Sooner or later every system jams. What sets a mature service apart from a fragile one is not never failing: it is knowing in advance how it fails, and having, ready to hand, the move that gets it going again.
Under the hood. The thing that really matters is not the list of possible failures, but what happens when one shows up: in most cases the system rights itself on its own, and in no case does a failure leave a wrong rating behind.
Almost all the anticipated hiccups need no manual intervention. If a region or a piece runs into trouble, the path falls back on its own to a backup route and resumes: the clash is rerouted to a nearby region, a search that finds no one falls back to a match against the AI, a rejected pair is re-proposed. For the player, the result is a match that starts anyway, not an error.
It is the most important guarantee of all. The server that hosts the clash has no power to touch ratings or coins: it can only sign a verdict and propose it. That verdict becomes real only after the services that hold the authority re-verify it on their own account. So, if a server goes down halfway through a match or a message is lost along the way, the effect is always «nothing happens», never «a wrong rating gets written»: even through failures, the rating stays consistent and reliable.
It is resilience by design: the worst outcome of a failure is a clean stop to pick up from, not a corrupted piece of data to fix by hand.