Retour au Blog

Les bots ont survécu à la panne de Yandex — l'architecture de votre backend de jeu devrait en faire autant

Publié le 20 octobre 2026
Les bots ont survécu à la panne de Yandex — l'architecture de votre backend de jeu devrait en faire autant Généré avec l'aide de l'IA

En bref

Découvrez comment la panne Yandex a révélé la migration des bots et comment architecturer votre backend de jeu pour survivre aux pannes majeures.

Votre tableau de bord de monitoring affiche une baisse de trafic de 40 % du jour au lendemain. Vous soufflez — la DDoS a dû s'arrêter. Mais l'analyse de Cloudflare sur la panne de Yandex d'octobre 2023 a prouvé que cette hypothèse était catastrophiquement fausse : les bots malveillants ne sont pas morts quand les data centers de Yandex sont tombés. Ils ont migré vers OVH, Hetzner et DigitalOcean en quelques minutes. Vos métriques agrégées vous ont menti pendant que les attaquants continuaient de tourner.

Si votre backend de jeu tourne sur un seul fournisseur cloud avec une détection de bots basée sur l'IP, vous êtes à un seul incident d'infrastructure du même angle mort. Ce runbook détaille ce que la panne de Yandex révèle sur le comportement des bots pendant les pannes d'infrastructure, et comment architecturer des backends de jeu qui y survivent.

Ce qui casse : quand votre fournisseur d'infrastructure tombe en panne

Quand un grand fournisseur d'infrastructure subit une panne, quatre systèmes critiques tombent simultanément — et ils tombent d'une manière qui ressemble à des améliorations pour un monitoring naïf.

Votre couche d'authentification se déconnecte. Si l'authentification de vos joueurs dépend du service d'identité d'un seul fournisseur, les joueurs ne peuvent pas se connecter. Pendant la panne de Yandex, les services étroitement couplés à l'infrastructure Yandex ont subi une interruption totale de service. Les joueurs ont vu des écrans d'erreur ; les bots qui ne s'authentifient pas ont continué à frapper des endpoints ailleurs.

Votre détection de bots perd son cadre de référence. La plupart des backends de jeu suivent les IP malveillantes par ASN ou par fournisseur d'hébergement. Quand ces IP s'éteignent, vos systèmes de détection les marquent comme « résolues » et cessent de chercher. Pendant ce temps, la même flotte de bots réapparaît sur de nouvelles IP chez un autre fournisseur.

Votre rate limiting devient inutile. Les limites de débit basées sur l'IP sont liées à des plages d'adresses spécifiques. Quand les bots migrent, ils obtiennent de nouvelles IP avec des scores de réputation propres. Votre rate limiter les traite comme un nouveau trafic légitime.

Vos agrégats de trafic deviennent de la désinformation. L'analyse de Cloudflare sur Yandex a montré que le trafic AS13238 (Yandex Search) était composé presque entièrement de crawlers légitimes — la baisse y était réelle. Mais AS200350 (Yandex.Cloud) a vu le scanning et le probing malveillants de vulnérabilités diminuer, tandis que l'automatisation à haut volume se relocalisait simplement. Les graphiques de trafic agrégé montrant une baisse de 99 % vous racontaient la mauvaise histoire.

Le mode de défaillance principal : votre monitoring pense que les choses se sont améliorées alors que la menace vient de se déplacer.

Comment détecter : repérer la migration des bots en temps réel

La détection nécessite un monitoring à plusieurs niveaux, pas seulement le volume de trafic agrégé. Chaque niveau capture un aspect différent du schéma de migration.

Niveau 1 : corrélation des sources de trafic

Au lieu de suivre le nombre total de requêtes par seconde, surveillez la distribution des sources de trafic entre les numéros de système autonome (ASN). Quand le trafic d'un fournisseur chute, vérifiez si le trafic d'autres fournisseurs augmente proportionnellement. Un schéma de chute-et-hausse corrélé entre les ASN est la signature d'une migration de bots.

Voici un détecteur Python qui identifie ce schéma :

import time
from collections import defaultdict
from dataclasses import dataclass
from typing import Dict, List, Optional

@dataclass
class TrafficSnapshot:
    timestamp: float
    source_asn: str
    request_count: int
    unique_ips: int
    error_rate: float

class BotMigrationDetector:
    """Detects correlated traffic drops and rises across ASNs
    to identify bot farm migration during infrastructure failures."""

    def __init__(self, migration_threshold: float = 0.3):
        self.history: Dict[str, List[TrafficSnapshot]] = defaultdict(list)
        self.migration_threshold = migration_threshold
        self.window_seconds = 300  # 5-minute rolling window

    def record_snapshot(self, snapshot: TrafficSnapshot):
        self.history[snapshot.source_asn].append(snapshot)
        self._prune_old_data()

    def _prune_old_data(self):
        cutoff = time.time() - self.window_seconds
        for asn in self.history:
            self.history[asn] = [
                s for s in self.history[asn]
                if s.timestamp > cutoff
            ]

    def detect_migration(self) -> Optional[Dict]:
        """Returns migration report if suspicious pattern detected."""
        declining_asns = []
        rising_asns = []

        for asn, snapshots in self.history.items():
            if len(snapshots) < 2:
                continue

            mid = len(snapshots) // 2
            first_half = snapshots[:mid]
            second_half = snapshots[mid:]

            avg_first = sum(s.request_count for s in first_half) / len(first_half)
            avg_second = sum(s.request_count for s in second_half) / len(second_half)

            if avg_first > 0:
                change_ratio = (avg_second - avg_first) / avg_first

                if change_ratio < -self.migration_threshold:
                    declining_asns.append({
                        'asn': asn,
                        'drop_pct': abs(change_ratio) * 100,
                        'baseline': avg_first
                    })
                elif change_ratio > self.migration_threshold:
                    rising_asns.append({
                        'asn': asn,
                        'rise_pct': change_ratio * 100,
                        'current': avg_second
                    })

        if declining_asns and rising_asns:
            return {
                'type': 'bot_migration',
                'declining': declining_asns,
                'rising': rising_asns,
                'confidence': self._calculate_confidence(declining_asns, rising_asns)
            }
        return None

    def _calculate_confidence(self, declining, rising) -> float:
        """Higher confidence when drop and rise magnitudes match."""
        total_drop = sum(d['drop_pct'] for d in declining)
        total_rise = sum(r['rise_pct'] for r in rising)

        if total_drop == 0:
            return 0.0

        ratio = min(total_rise, total_drop) / max(total_rise, total_drop)
        return round(ratio * 100, 2)


# Usage: detect Yandex-style migration
detector = BotMigrationDetector(migration_threshold=0.25)

# Yandex traffic collapsing
detector.record_snapshot(TrafficSnapshot(
    timestamp=time.time() - 240,
    source_asn="AS13238", request_count=15000,
    unique_ips=8500, error_rate=0.02
))
detector.record_snapshot(TrafficSnapshot(
    timestamp=time.time() - 120,
    source_asn="AS13238", request_count=1200,  # 92% drop
    unique_ips=900, error_rate=0.45
))

# OVH traffic surging (bots migrated here)
detector.record_snapshot(TrafficSnapshot(
    timestamp=time.time() - 240,
    source_asn="AS16276", request_count=5000,
    unique_ips=3200, error_rate=0.01
))
detector.record_snapshot(TrafficSnapshot(
    timestamp=time.time() - 120,
    source_asn="AS16276", request_count=18500,  # 270% increase
    unique_ips=2800, error_rate=0.08
))

migration = detector.detect_migration()
if migration:
    print(f"Bot migration detected: {migration['confidence']}% confidence")
    for d in migration['declining']:
        print(f"  {d['asn']}: -{d['drop_pct']:.0f}%")
    for r in migration['rising']:
        print(f"  {r['asn']}: +{r['rise_pct']:.0f}%")

Ce détecteur surveille les chutes et hausses de trafic corrélées entre plusieurs ASN. Quand AS13238 chute de 92 % et qu'AS16276 augmente simultanément de 270 %, il signale une probable migration de bots plutôt qu'un changement de trafic légitime. Le score de confiance augmente lorsque les ampleurs de chute et de hausse correspondent — une caractéristique d'une migration coordonnée.

Niveau 2 : empreinte comportementale

Ne vous contentez pas de suivre d'où vient le trafic — suivez comment il se comporte. Les bots qui migrent entre fournisseurs conservent des signatures comportementales identiques : les mêmes intervalles de requêtes, les mêmes schémas d'en-têtes, les mêmes durées de session et le même comportement de nouvelle tentative après erreur. La réputation IP se réinitialise quand ils bougent ; le comportement, non.

Construisez une empreinte comportementale pour chaque session et hachez-la :

import hashlib
import statistics

def fingerprint_session(requests: list[dict]) -> str:
    """Generate a behavioral fingerprint from a session's request pattern."""
    intervals = []
    for i in range(1, len(requests)):
        delta = requests[i]['timestamp'] - requests[i-1]['timestamp']
        intervals.append(round(delta, 3))

    if len(intervals) < 2:
        return "insufficient_data"

    # Key behavioral features
    interval_mean = round(statistics.mean(intervals), 3)
    interval_stdev = round(statistics.stdev(intervals), 3)
    total_duration = round(requests[-1]['timestamp'] - requests[0]['timestamp'], 3)
    error_count = sum(1 for r in requests if r.get('status_code', 200) >= 400)
    retry_count = sum(1 for r in requests if r.get('is_retry', False))
    unique_paths = len(set(r['path'] for r in requests))
    user_agent_count = len(set(r.get('user_agent', '') for r in requests))

    fingerprint_data = (
        f"{interval_mean}|{interval_stdev}|{total_duration}|"
        f"{error_count}|{retry_count}|{unique_paths}|{user_agent_count}"
    )

    return hashlib.sha256(fingerprint_data.encode()).hexdigest()[:16]


# Example: same bot session detected at two different providers
session_provider_a = [
    {'timestamp': 1000.0, 'path': '/api/login', 'status_code': 200},
    {'timestamp': 1000.5, 'path': '/api/profile', 'status_code': 200},
    {'timestamp': 1001.0, 'path': '/api/match/join', 'status_code': 429},
    {'timestamp': 1001.5, 'path': '/api/match/join', 'status_code': 200, 'is_retry': True},
    {'timestamp': 1002.0, 'path': '/api/score', 'status_code': 200},
]

session_provider_b = [
    {'timestamp': 2000.0, 'path': '/api/login', 'status_code': 200},
    {'timestamp': 2000.5, 'path': '/api/profile', 'status_code': 200},
    {'timestamp': 2001.0, 'path': '/api/match/join', 'status_code': 429},
    {'timestamp': 2001.5, 'path': '/api/match/join', 'status_code': 200, 'is_retry': True},
    {'timestamp': 2002.0, 'path': '/api/score', 'status_code': 200},
]

fp_a = fingerprint_session(session_provider_a)
fp_b = fingerprint_session(session_provider_b)

print(f"Provider A fingerprint: {fp_a}")
print(f"Provider B fingerprint: {fp_b}")
print(f"Same bot: {fp_a == fp_b}")

Quand vous voyez la même empreinte comportementale apparaître sur de nouvelles IP chez un autre fournisseur peu après la chute du fournisseur d'origine, c'est une migration confirmée — pas une coïncidence.

Niveau 3 : surveillance de l'état d'authentification

Suivez le ratio entre requêtes authentifiées et non authentifiées au fil du temps. Pendant les pannes d'infrastructure, les joueurs légitimes peinent à s'authentifier (ce qui déplace le ratio vers le non authentifié), tandis que les bots qui ne s'authentifient jamais continuent de frapper vos endpoints sans changement.

Ce niveau est particulièrement précieux car il sépare les vrais joueurs du trafic automatisé, quel que soit le fournisseur d'origine. Si vous utilisez les sessions liées au compte de horizOn avec plusieurs méthodes d'authentification (email, Google, Apple, anonyme), vous pouvez surveiller les taux de réussite d'authentification par méthode. Quand l'authentification par email échoue mais que celle par Google réussit, vous avez identifié une panne d'infrastructure partielle — pas une migration de bots. Quand toutes les authentifications réussissent mais que le trafic non authentifié augmente chez un nouveau fournisseur, c'est une migration.

Comment remédier : des stratégies de failover qui fonctionnent

La détection sans remédiation, c'est regarder votre jeu mourir en temps réel. Voici trois mécanismes de failover que vous pouvez implémenter cette semaine.

Failover basé sur le DNS

Configurez votre DNS avec des TTL courts et des health checks. Quand votre région principale tombe en panne, le DNS route automatiquement le trafic vers des régions secondaires. Cela ne nécessite aucune modification applicative.

Réglages recommandés :

  • TTL : 60-120 secondes (plus court signifie un failover plus rapide, mais plus de charge DNS)
  • Intervalle de health check : 10-15 secondes
  • Endpoint de health check : /health renvoyant 200 avec un temps de réponse < 500 ms
  • Déclencheur de failover : 3 échecs consécutifs du health check

Le compromis : des TTL courts augmentent le volume de requêtes DNS de 10 à 20 fois en fonctionnement normal. Pour un jeu avec 50 000 joueurs actifs quotidiens, cela ajoute environ 2 à 5 $/mois en coûts DNS — négligeable par rapport au downtime que cela évite.

Circuit breakers au niveau applicatif

Implémentez des circuit breakers dans votre client de jeu qui détectent quand un service échoue et routent automatiquement vers des alternatives :

using System;
using System.Net.Http;
using System.Threading.Tasks;

public class ServiceCircuitBreaker
{
    private readonly string _primaryEndpoint;
    private readonly string[] _fallbackEndpoints;
    private readonly int _failureThreshold;
    private int _failureCount;
    private DateTime _lastFailure;
    private CircuitState _state = CircuitState.Closed;
    private int _currentFallbackIndex;
    private readonly object _lock = new();

    public enum CircuitState { Closed, Open, HalfOpen }

    public ServiceCircuitBreaker(
        string primary, string[] fallbacks, int failureThreshold = 5)
    {
        _primaryEndpoint = primary;
        _fallbackEndpoints = fallbacks;
        _failureThreshold = failureThreshold;
    }

    public async Task&lt;HttpResponseMessage> SendRequest(HttpRequestMessage request)
    {
        lock (_lock)
        {
            if (_state == CircuitState.Open)
            {
                if (DateTime.UtcNow - _lastFailure > TimeSpan.FromSeconds(30))
                    _state = CircuitState.HalfOpen;
                else
                    return SendToFallback(request).GetAwaiter().GetResult();
            }
        }

        try
        {
            using var client = new HttpClient();
            client.Timeout = TimeSpan.FromSeconds(5);
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                lock (_lock) { _failureCount = 0; _state = CircuitState.Closed; }
                return response;
            }

            RecordFailure();
            return await SendToFallback(request);
        }
        catch (TaskCanceledException)
        {
            RecordFailure();
            return await SendToFallback(request);
        }
    }

    private void RecordFailure()
    {
        lock (_lock)
        {
            _failureCount++;
            _lastFailure = DateTime.UtcNow;
            if (_failureCount >= _failureThreshold)
                _state = CircuitState.Open;
        }
    }

    private async Task&lt;HttpResponseMessage> SendToFallback(HttpRequestMessage request)
    {
        for (int i = 0; i &lt; _fallbackEndpoints.Length; i++)
        {
            int idx = (_currentFallbackIndex + i) % _fallbackEndpoints.Length;
            try
            {
                var fallbackRequest = new HttpRequestMessage(
                    request.Method,
                    _fallbackEndpoints[idx] + request.RequestUri.PathAndQuery
                );
                using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
                var response = await client.SendAsync(fallbackRequest);
                if (response.IsSuccessStatusCode)
                {
                    _currentFallbackIndex = idx;
                    return response;
                }
            }
            catch { continue; }
        }

        throw new HttpRequestException("All endpoints failed");
    }
}

// Usage in your game client
var breaker = new ServiceCircuitBreaker(
    primary: "https://api-primary.game.example.com",
    fallbacks: new[] {
        "https://api-secondary.game.example.com",
        "https://api-tertiary.game.example.com"
    },
    failureThreshold: 3
);

var request = new HttpRequestMessage(HttpMethod.Get, "/api/leaderboard/global");
var response = await breaker.SendRequest(request);

Ce schéma évite les défaillances en cascade quand un fournisseur tombe. Comme nous l'avons vu dans notre analyse sur l'architecture des fallbacks serveur, le failover automatique fait la différence entre un hoquet de 30 secondes et une panne de 4 heures.

Rate limiting distribué

Quand le trafic bascule vers une nouvelle région, votre rate limiting doit fonctionner là-bas aussi. Les rate limits basés sur l'IP et locaux à chaque serveur deviennent inutiles pendant un failover, car le même bot repart d'une feuille blanche sur chaque endpoint.

Implémentez un rate limiting distribué à l'aide d'un store partagé accessible depuis toutes les régions :

import redis
import time

class DistributedRateLimiter:
    """Sliding window rate limiter backed by Redis.
    Works identically across all deployment regions."""

    def __init__(self, redis_url: str, max_requests: int = 100,
                 window_seconds: int = 60):
        self.redis = redis.from_url(redis_url)
        self.max_requests = max_requests
        self.window = window_seconds

    def is_allowed(self, identifier: str) -> tuple[bool, int]:
        """Returns (allowed, remaining_requests)."""
        key = f"ratelimit:{identifier}"
        now = time.time()
        window_start = now - self.window

        pipe = self.redis.pipeline()
        pipe.zremrangebyscore(key, 0, window_start)  # Remove expired entries
        pipe.zadd(key, {f"{now}:{id(identifier)}": now})  # Add current request
        pipe.zcard(key)  # Count requests in window
        pipe.expire(key, self.window)
        results = pipe.execute()

        request_count = results[2]
        remaining = max(0, self.max_requests - request_count)

        if request_count > self.max_requests:
            # Remove the request we just added — it's over limit
            pipe.zremrangebyscore(key, now, now)
            pipe.execute()
            return False, 0

        return True, remaining


# Usage: same limiter works from any region
limiter = DistributedRateLimiter(
    redis_url="redis://shared-redis.internal:6379",
    max_requests=60,   # 1 request per second average
    window_seconds=60
)

# Identify by player ID, not IP — survives migration
allowed, remaining = limiter.is_allowed("player:8f3a2b1c")

Remarquez que l'identifiant utilise l'ID du joueur, pas l'adresse IP. Les bots qui migrent vers de nouvelles IP conservent la même empreinte de joueur. Cette approche les attrape même après une migration au niveau du fournisseur.

Comment prévenir : des schémas d'architecture pour la résilience

La remédiation est réactive. La prévention consiste à construire une architecture qui suppose que la panne d'infrastructure arrivera — parce qu'elle arrivera.

Schéma 1 : déploiement multi-fournisseurs pour les services critiques

Déployez votre authentification, votre matchmaking et votre détection de bots sur au moins deux fournisseurs cloud. Cela ne signifie pas faire tourner toute votre stack deux fois — cela signifie que les services qui doivent survivre à une panne sont indépendants du fournisseur.

Réalité des coûts : Le déploiement multi-fournisseurs pour les services critiques ajoute généralement 30 à 50 % à votre facture d'infrastructure. Pour un backend de jeu servant 10 000 joueurs simultanés, cela représente environ 800 à 1 500 $/mois supplémentaires. Le coût d'une panne de 2 heures pour le même jeu — en tenant compte des revenus IAP perdus, du churn des joueurs et des tickets support — est en moyenne de 5 000 à 12 000 $.

Multi-fournisseurs minimal viable :

  • Authentification : fournisseur principal + secondaire
  • Données joueur / état de sauvegarde : répliqués entre les fournisseurs
  • Matchmaking : au moins une région de fallback
  • Classements et analytics : peuvent tolérer de brèves pannes (file d'attente et rejeu)

Schéma 2 : détection de bots sans état et indépendante du fournisseur

Concevez votre détection de bots pour qu'elle fonctionne à l'identique, quel que soit le lieu d'origine du trafic. La panne de Yandex a prouvé que la détection basée sur l'IP est fondamentalement fragile — les bots obtiennent de nouvelles IP dès qu'ils migrent.

Construisez la détection autour de caractéristiques comportementales qui survivent aux changements d'infrastructure :

from dataclasses import dataclass
from typing import List

@dataclass
class SessionFeatures:
    request_count: int
    duration_seconds: float
    interval_variance: float      # Low variance = bot-like regularity
    error_retry_rate: float       # Bots retry aggressively; humans quit
    unique_endpoints: int         # Bots hit predictable endpoint patterns
    header_consistency: float     # Bots reuse identical headers

class BehavioralClassifier:
    """Bot classifier based on behavioral features, not IP addresses.
    Survives provider migration because behavior doesn't change."""

    # Weights calibrated from production bot traffic analysis
    WEIGHTS = {
        'interval_variance': 0.30,   # Most discriminating feature
        'error_retry_rate': 0.25,
        'header_consistency': 0.20,
        'endpoint_diversity': 0.15,
        'session_duration': 0.10,
    }

    def classify(self, features: SessionFeatures) -> float:
        """Returns bot probability from 0.0 (human) to 1.0 (bot)."""
        score = 0.0

        # Low interval variance → bot (humans are irregular)
        if features.interval_variance &lt; 0.1:
            score += self.WEIGHTS['interval_variance']
        elif features.interval_variance &lt; 0.3:
            score += self.WEIGHTS['interval_variance'] * 0.5

        # High retry rate → bot (humans give up after 1-2 failures)
        if features.error_retry_rate > 0.8:
            score += self.WEIGHTS['error_retry_rate']
        elif features.error_retry_rate > 0.5:
            score += self.WEIGHTS['error_retry_rate'] * 0.5

        # Perfect header consistency → bot (humans have browser variations)
        if features.header_consistency > 0.95:
            score += self.WEIGHTS['header_consistency']

        # Very few unique endpoints → bot (humans explore)
        if features.unique_endpoints &lt;= 3 and features.request_count > 20:
            score += self.WEIGHTS['endpoint_diversity']

        # Suspiciously precise session duration (e.g., exactly 300s)
        if abs(features.duration_seconds % 60) &lt; 2:
            score += self.WEIGHTS['session_duration']

        return min(score, 1.0)


# Usage
classifier = BehavioralClassifier()

bot_session = SessionFeatures(
    request_count=150, duration_seconds=300.0,
    interval_variance=0.02, error_retry_rate=0.95,
    unique_endpoints=2, header_consistency=0.99
)

human_session = SessionFeatures(
    request_count=45, duration_seconds=1847.3,
    interval_variance=0.67, error_retry_rate=0.15,
    unique_endpoints=12, header_consistency=0.73
)

print(f"Bot score: {classifier.classify(bot_session):.2f}")   # ~0.85
print(f"Human score: {classifier.classify(human_session):.2f}")  # ~0.10

Ce classifieur ne se soucie pas de l'ASN d'origine de la requête. Quand un bot migre de Yandex.Cloud vers OVH, son empreinte comportementale reste la même — et le classifieur l'attrape toujours.

Schéma 3 : découpler l'authentification de l'infrastructure

Votre système d'authentification devrait survivre à toute panne d'infrastructure unique. Utilisez plusieurs fournisseurs d'authentification afin que les joueurs aient des options de fallback pour accéder à leurs comptes.

Implémentez une authentification avec dégradation gracieuse :

class MultiProviderAuth:
    """Authentication with automatic provider fallback."""

    def __init__(self, providers: list):
        # Order by reliability — primary first
        self.providers = providers

    async def authenticate(self, credentials: dict) -> dict:
        last_error = None

        for provider in self.providers:
            try:
                result = await provider.authenticate(credentials)
                return {
                    'success': True,
                    'provider': provider.name,
                    'player_id': result['player_id'],
                    'session_token': result['session_token']
                }
            except Exception as e:
                last_error = e
                continue

        return {
            'success': False,
            'error': 'All authentication providers failed',
            'last_error': str(last_error)
        }

Ce schéma est important car pendant la panne de Yandex, les services étroitement couplés à l'infrastructure d'identité de Yandex ne pouvaient pas authentifier les utilisateurs du tout. Les joueurs se sont retrouvés verrouillés dehors ; les bots qui ne s'authentifient pas ont continué de tourner. Une stratégie d'authentification multi-fournisseurs signifie que vos joueurs continuent de jouer même quand votre fournisseur d'authentification principal est en panne.

Schéma 4 : observabilité couvrant plusieurs fournisseurs

Votre monitoring doit voir à travers tous les fournisseurs simultanément. Divisez le monitoring en préoccupations distinctes :

Monitoring de la distribution du trafic : Suivez le volume de requêtes par ASN, par région, par fournisseur. Alertez quand la distribution change de plus de 20 % sur une fenêtre de 5 minutes.

Monitoring de la santé de l'authentification : Suivez les taux de réussite d'authentification par méthode et par fournisseur. Alertez quand le taux de réussite d'un fournisseur passe sous 95 %.

Monitoring de la détection de bots : Suivez le ratio des sessions signalées par rapport au total, par fournisseur. Alertez quand le ratio passe sous la baseline chez un fournisseur pendant qu'il augmente chez un autre — c'est une migration.

Corrélation inter-fournisseurs : C'est la pièce critique. Les métriques d'un fournisseur individuel peuvent sembler saines pendant que le système global est attaqué. Construisez des tableaux de bord qui montrent tous les fournisseurs côte à côte pour repérer les schémas de migration d'un coup d'œil.

Bonnes pratiques : 5 étapes vers une architecture de backend de jeu résiliente aux bots

  1. Surveillez la migration, pas seulement le volume. Mettez en place une corrélation de trafic inter-fournisseurs qui détecte quand le trafic chute à un endroit et augmente à un autre. Les métriques agrégées seules vous auraient trompé pendant la panne de Yandex — le trafic AS200350 a chuté, mais l'activité malveillante a continué chez OVH et Hetzner.

  2. Implémentez des circuit breakers sur chaque dépendance externe. Votre client de jeu, votre service de classement et votre logique de matchmaking ont tous besoin d'un failover automatique. Le schéma du circuit breaker évite les défaillances en cascade quand un fournisseur tombe. Testez-les mensuellement en tuant des endpoints en staging.

  3. Utilisez la détection comportementale, pas la détection par IP. L'analyse comportementale (intervalles de requêtes, taux de nouvelle tentative, schémas d'en-têtes) fonctionne quel que soit l'hébergement des bots. La détection basée sur l'IP échoue dès que les bots migrent vers un nouveau fournisseur avec une réputation IP propre. Le classifieur comportemental ci-dessus attrape les bots qui survivent à des chutes de trafic de 99 %.

  4. Concevez l'authentification pour une dégradation gracieuse. Utilisez les sessions liées au compte de horizOn avec plusieurs méthodes d'authentification (email, Google, Apple, anonyme). Quand un fournisseur d'authentification est indisponible, les joueurs peuvent utiliser une autre méthode pour conserver l'accès à leur état de jeu. Cela évite le pire scénario : des joueurs verrouillés dehors pendant que les bots continuent de tourner.

  5. Testez votre failover chaque trimestre. Simulez des pannes de fournisseurs en staging. Vérifiez que le failover DNS se déclenche dans votre TTL, que les circuit breakers s'ouvrent correctement et que le rate limiting fonctionne dans les régions de fallback. Documentez le temps entre la détection de panne et la récupération complète. Un failover non testé n'est pas un failover — c'est de l'espoir.

Conclusion

La panne de Yandex a prouvé que les acteurs malveillants ont une meilleure résilience d'infrastructure que la plupart des backends de jeu. Les bots ne sont pas morts quand les data centers de Yandex sont tombés — ils ont migré vers d'autres fournisseurs en quelques minutes et ont continué de tourner. Votre tableau de bord de trafic agrégé montrait une amélioration. La réalité empirait.

Commencez par déployer le détecteur de bots comportemental de cet article dans votre environnement de staging. Faites-le tourner pendant une semaine pour établir des baselines, puis simulez une panne de fournisseur en bloquant le trafic d'un ASN et observez si le détecteur de migration se déclenche. Les schémas que vous découvrirez révéleront exactement où votre architecture actuelle est vulnérable.

Pour les studios de jeux qui construisent des systèmes d'authentification et de données joueur capables de survivre aux pannes d'infrastructure, horizOn fournit des sessions liées au compte avec sauvegarde cloud avec gestion de révisions — des services conçus pour que vos joueurs conservent leur progression même quand l'inattendu se produit. Essayez-le gratuitement ou explorez la documentation API pour voir comment les sessions liées au compte s'intègrent à votre backend existant.


Source : Panne de Yandex : les données montrent qu'au lieu de s'éteindre, les bots malveillants se sont déplacés