Que se passe-t-il quand 700 000 joueurs arrivent sur votre jeu multijoueur indie en même temps (et comment y survivre)
En bref
Apprenez comment votre backend peut survivre à 700 000 joueurs simultanés grâce aux stratégies de Goose Goose Duck : lobby, atomicité Redis, file d'attente et dégradation gracieuse.
Tout développeur indie a déjà rêvé du succès viral du jour au lendemain. Votre jeu explose sur Twitch, le nombre de joueurs simultanés sur Steam passe de 200 à 200 000 en une semaine, et soudainement, tout le monde ne parle que de vous. Ce que personne ne vous dit dans ce rêve, c'est à quoi ressemble votre backend à 3 heures du matin quand votre service de matchmaking est en surchauffe, que votre base de données de lobbies génère des erreurs d'écriture concurrente, et que votre Discord est rempli de joueurs incapables de se connecter à une seule partie.
Ce n'est pas hypothétique. Quand Goose Goose Duck est devenu viral fin 2022, Gaggle Studios — une petite équipe sans expérience de méga-succès — a vu le nombre de joueurs simultanés dépasser les 700 000. Leur backend a tenu. Pas parce qu'ils avaient des ressources illimitées, mais parce qu'ils avaient pris des décisions architecturales spécifiques très tôt qui leur ont permis de survivre au pic.
Cet article détaille exactement quelles ont été ces décisions, ce qui casse en premier quand un jeu multijoueur devient viral, et les schémas concrets que vous pouvez appliquer à votre propre projet avant l'arrivée du trafic.
L'anatomie d'un pic viral multijoueur
Ce qui casse réellement (dans l'ordre)
Quand un jeu multijoueur atteint 10 à 100 fois sa charge attendue, les défaillances s'enchaînent dans un ordre prévisible. Comprendre cet ordre est crucial car vous devez renforcer votre système dans la bonne séquence.
1. Authentification et connexion (5 à 15 fois la charge normale dans les premières 48 heures)
Chaque joueur qui veut jouer doit d'abord s'authentifier. Le backend de Steam gère la partie lourde pour les jeux authentifiés via Steam, mais votre serveur doit quand même valider les tickets, créer ou récupérer les profils des joueurs, et renvoyer des jetons de session. Si chaque requête d'authentification touche votre base de données principale, vous avez un problème. Une rafale de 50 000 requêtes de connexion par minute, chacune écrivant dans PostgreSQL pour la création de session, va saturer votre pool de connexions en moins de 90 secondes.
2. Découverte de lobbies et matchmaking (10 à 50 fois la charge normale)
C'est le premier domino qui tue réellement l'expérience joueur. Quand 200 000 joueurs parcourent les lobbies simultanément, votre modèle de requêtes de liste de lobbies passe de « centaines de lectures par seconde » à « dizaines de milliers de lectures par seconde ». Si l'état des lobbies réside dans votre base de données relationnelle principale, vous vous retrouvez à lutter contre des réplicas en lecture qui ne peuvent pas suivre le retard de réplication, renvoyant des données de lobbies périmées qui montrent des salles comme disponibles alors qu'elles sont déjà pleines.
3. Création et jointure de lobbies (pic d'écritures)
Chaque nouveau lobby est une écriture. Chaque joueur qui rejoint un lobby est une écriture (mise à jour de la liste des joueurs). Chaque joueur qui quitte est une écriture. Au pic de trafic de Goose Goose Duck, cela signifiait des milliers de mutations d'état de lobby par seconde. Gaggle Studios utilisait un modèle peer-to-peer pour le gameplay réel, mais la coordination des lobbies nécessitait une centralisation — les joueurs doivent se trouver avant de pouvoir se connecter directement.
4. Traversée NAT et établissement de connexions P2P
C'est là que l'architecture pair-à-pair atteint sa limite. Même avec une infrastructure STUN/TURN, les connexions P2P échouent. La moyenne industrielle pour le succès des connexions P2P sans recours au relais est d'environ 75 à 85 % des paires de joueurs. Les 15 à 25 % restants ont besoin de serveurs relais TURN. Avec 700 000 joueurs simultanés tentant des milliers de connexions par seconde, vous avez besoin d'une infrastructure de relais que la plupart des équipes indies n'ont jamais construite.
Pourquoi le P2P était la bonne décision (jusqu'à ce qu'elle ne le soit plus)
Gaggle Studios a choisi le peer-to-peer pour le gameplay réel de Goose Goose Duck, et pour un jeu de déduction sociale avec 2 à 16 joueurs par session, c'était véritablement la bonne décision. Voici pourquoi, et où les compromis se sont manifestés.
Le modèle de coût du P2P
Considérez le calcul. Une architecture de serveur dédié pour une partie de 16 joueurs durant 15 minutes sur une instance cloud modeste (~0,04 $/heure pour un vCPU partagé) coûte environ 0,01 $ par partie. Multipliez par 500 000 parties simultanées aux heures de pointe, et vous regardez 5 000 $/heure rien que pour le calcul. Soit 120 000 $ par jour.
Le P2P reporte ce coût de calcul sur la machine du joueur hôte. Le coût de votre infrastructure se réduit à la couche de coordination : serveurs de matchmaking, état des lobbies, authentification et relais STUN/TURN. Pour Goose Goose Duck, cela signifiait que leur facture d'infrastructure restait gérable même lorsque le nombre de joueurs grimpait en flèche.
Le plafond de fiabilité du P2P
Mais le P2P introduit des modes de défaillance que les serveurs dédiés n'ont pas :
- Migration de l'hôte : quand le joueur hôte se déconnecte, la session doit transférer l'autorité à un autre pair. Pour un jeu de déduction sociale, une migration ratée signifie une perte de l'état des votes, des attributions de rôles désynchronisées et une partie ruinée. Une séquence typique de migration d'hôte ressemble à ceci :
// Simplified peer-to-peer host migration logic
// When the current host becomes unreachable
void OnHostUnreachable(float timeoutSeconds = 3.0f) {
// 1. All peers detect host disconnect via heartbeat timeout
// 2. Each peer independently evaluates whether it should become the new host
TArray<FPlayerInfo> remainingPeers = GetConnectedPeers();
FPlayerInfo newHost = SelectNewHost(remainingPeers); // Lowest latency, highest bandwidth
if (newHost.PlayerId == GetLocalPlayerId()) {
// This peer becomes the new host
BecomeHost();
// Reconstruct authoritative game state from local cache
GameState = ReconstructFromLastKnownState();
// Tell all other peers to connect to the new host
BroadcastHostMigration(newHost.Address);
// Resume gameplay - votes, timers, and role assignments must survive this transition
ResumeSessionWithReconciledState();
} else {
// Wait for migration signal, then connect to new host
ConnectToNewHost(newHost.Address, timeoutSeconds);
}
}
Chacune de ces étapes est un point de défaillance potentiel. Si deux pairs décident tous deux de devenir l'hôte (scénario de split-brain), vous obtenez deux états de jeu divergents qui ne peuvent pas être reconciliés.
Échecs de traversée NAT : les joueurs derrière des NAT symétriques ou des NAT de niveau opérateur ne peuvent pas établir de connexions directes. Votre infrastructure relais TURN doit absorber ces joueurs. À grande échelle, 15 % de 700 000 joueurs simultanés, c'est 105 000 joueurs nécessitant du trafic relais — et la bande passante du relais est chère, généralement 0,05 à 0,10 $ par Go.
Vulnérabilité à la triche : la machine du joueur hôte fait autorité. Toute donnée côté client peut être manipulée. Pour un jeu de party occasionnel, c'est moins catastrophique que pour un shooter compétitif, mais cela dégrade tout de même l'expérience. Les conceptions serveur-autoritaires (comme celles utilisées dans les propositions d'optimisation des serveurs de Fortnite) éliminent des catégories entières d'exploits, mais elles nécessitent du calcul dédié.
La couche de matchmaking : construire pour 10 fois votre pic attendu
C'est la section dont la plupart des développeurs indies ont besoin mais qu'ils sautent jusqu'à ce qu'il soit trop tard. Votre système de matchmaking est la porte d'entrée de votre jeu. S'il est lent, les joueurs partent. S'il est cassé, les joueurs ne peuvent pas jouer.
Architecture de gestion de l'état des lobbies
Le système de lobbies de Goose Goose Duck devait gérer ces opérations à grande échelle :
- Parcourir les lobbies (lectures intensives) : les joueurs filtrent et listent les lobbies disponibles
- Créer un lobby (écriture) : un nouvel enregistrement de lobby avec paramètres de jeu, région et capacité
- Rejoindre un lobby (écriture conditionnelle) : opération atomique – vérifier la capacité, ajouter le joueur, ou échouer
- Quitter un lobby (écriture + suppression possible) : retirer le joueur, supprimer le lobby s'il est vide
- Mettre à jour les paramètres du lobby (écriture) : l'hôte modifie les paramètres du jeu
Voici un gestionnaire de lobby simplifié qui gère l'opération atomique de jointure, la plus sujette aux erreurs en charge :
import asyncio
from dataclasses import dataclass, field
from typing import Optional
import uuid
@dataclass
class Lobby:
lobby_id: str
host_id: str
max_players: int
players: list = field(default_factory=list)
region: str = "us-east"
game_settings: dict = field(default_factory=dict)
created_at: float = 0.0
class LobbyManager:
def __init__(self, cache_client, db_client):
self.cache = cache_client # Redis or similar
self.db = db_client # PostgreSQL or similar
self.MAX_LOBBIES_PER_REGION = 10000
self.LOBBY_TTL_SECONDS = 3600 # Auto-cleanup stale lobbies
async def join_lobby(self, lobby_id: str, player_id: str) -> dict:
"""
Atomic join operation using Redis optimistic locking.
Prevents the race condition where two players simultaneously
join a lobby that has one slot remaining.
"""
cache_key = f"lobby:{lobby_id}"
# Use a Lua script for atomic check-and-modify in Redis
# This is the critical path—under viral load, this single
# operation runs thousands of times per second
lua_script = """
local key = KEYS[1]
local player_id = ARGV[1]
local max_players = tonumber(ARGV[2])
local lobby_data = redis.call('HGETALL', key)
if #lobby_data == 0 then
return {-1, "lobby_not_found"}
end
-- Parse the player count from the hash
local current_players = tonumber(redis.call('HGET', key, 'player_count'))
if current_players == nil then
return {-1, "corrupted_state"}
end
if current_players >= max_players then
return {0, "lobby_full"}
end
-- Atomic increment and add player
redis.call('HINCRBY', key, 'player_count', 1)
redis.call('SADD', key .. ':players', player_id)
redis.call('EXPIRE', key, 3600)
return {1, "joined"}
"""
result = await self.cache.eval(
lua_script,
keys=[cache_key],
args=[player_id, str(self.MAX_PLAYERS)]
)
status_code, message = result
if status_code == -1:
raise LobbyNotFoundException(message)
elif status_code == 0:
raise LobbyFullException(message)
# Async write to persistent DB (non-blocking, eventual consistency is fine here)
asyncio.create_task(self._persist_join(lobby_id, player_id))
return {"status": "joined", "lobby_id": lobby_id}
async def _persist_join(self, lobby_id: str, player_id: str):
"""Background persistence—lobby state in Redis is the source of truth for joins.
DB only lags by milliseconds but is not on the critical path."""
await self.db.execute(
"UPDATE lobbies SET player_count = player_count + 1, "
"updated_at = NOW() WHERE lobby_id = $1",
lobby_id
)
await self.db.execute(
"INSERT INTO lobby_players (lobby_id, player_id, joined_at) "
"VALUES ($1, $2, NOW()) ON CONFLICT DO NOTHING",
lobby_id, player_id
)
Le détail clé ici est le Lua script dans Redis. Une implémentation naïve qui fait un GET, vérifie la capacité dans le code applicatif, puis fait un POST crée une fenêtre de concurrence où 15 joueurs peuvent rejoindre un lobby de 16 joueurs simultanément, résultant en 17 joueurs et une logique de jeu cassée. Le Lua script s'exécute atomiquement dans Redis — pas de race condition, pas de jointures perdues, même à des milliers d'opérations par seconde.
Transfert de connexion : du lobby au gameplay
Une fois un lobby plein, le jeu doit passer de la coordination centralisée du lobby au gameplay peer-to-peer. Ce transfert est l'endroit où la plupart des jeux multijoueurs indies introduisent des pics de latence ou des échecs purs et simples.
Le schéma qui fonctionne :
- Le joueur hôte ouvre un WebSocket ou un socket d'écoute UDP
- Le serveur (système de lobby) distribue l'IP et le port de l'hôte à tous les pairs
- Les pairs tentent une connexion P2P directe via STUN
- Si STUN échoue dans les N secondes, repli sur le relais TURN
- Une fois que tous les pairs signalent la connexion, l'hôte signale le début de la partie
Pour la communication en temps réel pendant ce transfert, les connexions WebSocket sont bien plus fiables que le HTTP polling, surtout quand vous devez envoyer des mises à jour d'état de connexion à 8 à 16 clients simultanément.
Façonnage du trafic pendant un pic viral
L'une des choses les plus intelligentes que l'équipe de Goose Goose Duck a faites a été de gérer les attentes pendant le pic de trafic. Quand votre backend est à capacité, vous avez deux options : laisser tout se dégrader de manière imprévisible (déconnexions aléatoires, état de lobby corrompu, erreurs de timeout), ou implémenter une dégradation gracieuse.
Schémas de dégradation gracieuse
File d'attente de connexion : au lieu de rejeter les joueurs quand les serveurs de lobby sont à capacité, placez-les dans une file d'attente virtuelle avec un compteur de position en temps réel. Les joueurs attendront 2 minutes. Ils ne toléreront pas un message cryptique « erreur serveur ».
// C# connection queue with position feedback
public class ConnectionQueue
{
private readonly ConcurrentQueue<string> _queue = new();
private readonly SemaphoreSlim _admissionGate;
private readonly int _maxConcurrentSessions;
public ConnectionQueue(int maxConcurrentSessions)
{
_maxConcurrentSessions = maxConcurrentSessions;
_admissionGate = new SemaphoreSlim(maxConcurrentSessions, maxConcurrentSessions);
}
public async Task<QueueResult> TryEnterQueue(string playerId)
{
int position = _queue.Count + 1;
_queue.Enqueue(playerId);
// Estimate wait time: assume ~30 second average session search time
// at current throughput
int estimatedWaitSeconds = (position / _maxConcurrentSessions) * 30;
if (_admissionGate.CurrentCount > 0)
{
await _admissionGate.WaitAsync();
_queue.TryDequeue(out _);
return new QueueResult { Admitted = true, Position = 0 };
}
return new QueueResult
{
Admitted = false,
Position = position,
EstimatedWaitSeconds = estimatedWaitSeconds
};
}
}
Répartition régionale de charge : si US-Est est saturé mais que UE-Ouest a de la capacité, redirigez les nouveaux joueurs américains vers l'UE avec un avertissement de latence plutôt que de refuser la connexion. Un ping de 120 ms dans un jeu de déduction sociale est pratiquement imperceptible — ce ne sont pas des jeux de combat à la frame parfaite.
Limitation du taux de création de lobbies : pendant le pic de charge, limitez la création de lobbies à un lobby par joueur toutes les 30 secondes. Cela empêche le spam de lobbies par bots (un vrai problème pour Goose Goose Duck) et réduit la pression d'écriture sur la base de données de lobbies.
Répartition des coûts : ce que coûte réellement une échelle virale
Mettons des chiffres réels là-dessus. Voici un modèle de coût approximatif pour un événement viral de l'ampleur de Goose Goose Duck avec différentes architectures backend :
P2P avec coordination centralisée des lobbies (ce qu'a fait Goose Goose Duck) :
| Composant | Coût mensuel (à 700 000 joueurs simultanés de pointe) |
|---|---|
| Serveurs de lobby/matchmaking (12 instances c5.2xlarge, auto-scaled) | 3 500 $ – 5 000 $ |
| Cluster Redis pour l'état des lobbies (3 nœuds, r6g.xlarge) | 1 800 $ |
| Serveurs relais TURN (pour 15 % du trafic, ~100 000 joueurs) | 8 000 $ – 15 000 $ |
| PostgreSQL pour l'état persistant (RDS Multi-AZ) | 600 $ |
| Bande passante (coordination des lobbies, ~2 To/jour) | 1 200 $ |
| Total | 15 100 $ – 23 600 $/mois |
Serveurs entièrement dédiés (chaque partie sur une VM cloud) :
| Composant | Coût mensuel (à 700 000 joueurs simultanés de pointe) |
|---|---|
| Serveurs de jeu (~50 000 parties simultanées × 0,04 $/h) | 1 440 000 $/mois |
| Matchmaking et lobby | 5 000 $ |
| Infrastructure de base de données | 2 000 $ |
| Total | ~1 447 000 $/mois |
La différence de coût est de deux ordres de grandeur. Pour un jeu free-to-play qui gagne de l'argent grâce aux cosmétiques, le modèle de serveur dédié est une voie directe vers la faillite à moins que la monétisation ne soit agressive dès le premier jour. Le P2P n'est pas une architecture paresseuse — c'est une décision financière délibérée.
Cependant, les économies s'accompagnent de compromis. La gravité de la triche augmente. La qualité de la connexion varie selon l'hôte. Et votre infrastructure de coordination doit être infaillible car c'est le point de défaillance unique pour chaque partie de votre jeu.
Si vous évaluez ces compromis pour votre propre projet, horizOn gère la couche de coordination — gestion des lobbies, matchmaking, authentification des joueurs et état de session — afin que vous puissiez vous concentrer sur le gameplay plutôt que sur l'infrastructure. La plateforme a été construite spécifiquement pour ce cas d'usage : les équipes indies qui ont besoin de passer à l'échelle sans consacrer des mois à l'ingénierie backend.
5 schémas d'architecture backend pour survivre à la croissance virale
Voici les schémas concrets que vous devriez implémenter avant d'en avoir besoin, car les adapter pendant un pic de trafic transforme les incendies backend en enterrements backend :
1. Séparez l'état des lobbies de l'état du jeu
Votre système de coordination des lobbies et votre réseau de gameplay réel sont des systèmes différents avec des profils de mise à l'échelle différents. L'état des lobbies est à forte lecture, écriture modérée, et bénéficie du caching (Redis). L'état du jeu est à haute fréquence, faible latence, et appartient à la machine hôte ou à un serveur dédié. Les mélanger dans une seule base de données est un piège mortel pour la mise à l'échelle.
2. Utilisez des opérations atomiques pour les écritures sensibles à la capacité
L'opération de jointure de lobby que j'ai montrée ci-dessus est atomique via Redis Lua. Ne vous fiez pas au verrouillage au niveau applicatif pour quoi que ce soit qui détermine si une salle de jeu déborde. À 2 000 jointures par seconde, même une fenêtre de concurrence de 10 ms signifie 20 lobbies surbookés.
3. Implémentez la file d'attente de connexion avant d'en avoir besoin
Une file d'attente avec un temps d'attente estimé de 60 secondes retient 70 à 80 % des joueurs. Un message générique « échec de connexion » en retient environ zéro. Intégrez le système de file d'attente dans votre architecture initiale. Vous pouvez le désactiver quand le trafic est faible, mais vous ne pouvez pas le construire assez vite quand le trafic explose.
4. Surveillez les transitions lobby→jeu séparément
La plupart des systèmes de surveillance suivent le « nombre total de joueurs en ligne » et le « taux d'erreur ». Vous avez besoin de métriques spécifiques pour le point de transition : quel pourcentage de lobbies pleins passent avec succès au gameplay ? Si ce nombre descend en dessous de 95 %, votre infrastructure STUN/TURN ou votre logique de trouée P2P échoue. C'est la métrique qui prédit le churn des joueurs plus précisément que toute autre.
5. Construisez une échelle de dégradation gracieuse
Définissez vos conditions de dégradation à l'avance :
- Vert (moins de 80 % de capacité) : Fonctionnalité complète, aucune restriction
- Jaune (80–95 % de capacité) : Activer les limites de taux de création de lobbies, favoriser la jointure aux lobbies existants
- Orange (95–100 % de capacité) : Activer la file d'attente de connexion, désactiver les filtres de matchmaking, accepter les parties cross-région
- Rouge (au-dessus de la capacité) : File d'attente complète, page de repli statique pour les nouvelles connexions, prioriser les sessions existantes
Écrivez ces seuils dans votre configuration d'infrastructure. Mettez en place des alertes à chaque frontière. La différence entre un « moment viral » et un « désastre viral » est de savoir si vous atteignez l'Orange avant le Rouge.
La leçon plus large
L'histoire de Goose Goose Duck démontre quelque chose de fondamental sur l'architecture des jeux multijoueurs : la décision entre P2P et serveurs dédiés n'est pas une décision de qualité — c'est une décision économique et architecturale avec des conséquences en cascade. Le P2P a potentiellement économisé des millions de dollars à Gaggle Studios en coûts de serveur, mais il nécessitait une couche de coordination robuste, une gestion minutieuse des lobbies et la volonté d'accepter certains compromis de qualité.
Pour les développeurs indies qui planifient leur architecture multijoueur, le message est clair : concevez pour votre pic, pas pour votre moyenne. Votre backend subira 50 à 100 fois sa charge normale le jour où votre jeu deviendra viral. Si vous n'avez pas testé à cette échelle, vous n'êtes pas prêt.
Commencez par la couche de lobby et de matchmaking. Faites en sorte que les opérations atomiques de lobby soient correctes. Construisez la file d'attente de connexion. Implémentez le basculement régional. Ce sont les composants qui déterminent si votre moment viral est une success story ou un post-mortem.
Si vous voulez sauter des mois d'ingénierie backend et livrer avec une couche de coordination déjà éprouvée à grande échelle, horizOn fournit la gestion des lobbies, le matchmaking et l'état de session prêts à l'emploi — afin que vous puissiez vous concentrer à rendre votre jeu amusant au lieu de vous demander si vos serveurs tiendront. Consultez les documents de l'API pour voir comment cela s'intègre à votre architecture.
Source : Staying Lean : Comment nous avons construit le plus grand jeu de déduction sociale du monde