Terug naar Blog

CDN-schaling voor game-assets: een runbook om verkeerspieken op de launchedag te overleven

Gepubliceerd op 4 augustus 2026
CDN-schaling voor game-assets: een runbook om verkeerspieken op de launchedag te overleven Gegenereerd met behulp van AI

Kort samengevat

Ontdek hoe je CDN-schaling voor game-assets optimaliseert en verkeerspieken op de launchedag overleeft met CDN-caching, shielding en failover.

Je CDN zal bezwijken — zo weet je wanneer

Elke game-ontwikkelaar vreest hetzelfde lanceerdagscenario: je Steam-pagina gaat live, het aantal spelers stijgt boven de 10.000 gelijktijdige gebruikers, en plots haperen texturen-downloads op 200ms p99-latency in plaats van de gebruikelijke 12ms. Spelers melden ontbrekende modellen. Patch-downloads blijven steken op 43%. Je monitoringdashboard kleurt rood en je hebt geen idee welke laag faalt.

Dit is niet hypothetisch. cdnjs — een van de meest gebruikte open-source CDN-netwerken ter wereld — heeft onlangs een volledige infrastructuurmigratie naar Cloudflare's Developer Platform afgerond om 9 miljard verzoeken per dag te verwerken. Het migratieverhaal onthult architectuurpatronen die direct van toepassing zijn op game-assetlevering, waar een enkele update van een 4K-textuurpakket in enkele minuten terabytes aan verkeer kan genereren.

De kernles: CDN-schaling voor game-assets draait niet om het kopen van meer bandbreedte. Het gaat om het ontwerpen van cachehiërarchieën, fallbacklogica en origin shielding zodat verkeerspieken geen storingen worden, maar non-events.

Dit runbook behandelt wat er kapotgaat wanneer je CDN verzadigd raakt, hoe je verzadiging detecteert voordat je Discord volloopt met woede, hoe je in productie herstelt, en hoe je architectuur ontwerpt om herhaling te voorkomen.


Wat er kapotgaat wanneer je CDN verzadigt

Game-assetlevering heeft een uniek verkeersprofiel vergeleken met standaard webcontent. Om de faalmodi te begrijpen, moet je dat profiel begrijpen.

Het probleem van de verkeersvorm

Een typische indie-multiplayer game kent deze verkeerspatronen:

  • Baseline: 50-200 verzoeken/sec voor lobby-assets, UI-sprites, configuratie-JSON
  • Patchdagpiek: 15.000-80.000 verzoeken/sec binnen een venster van 3 minuten wanneer Steam automatische updates activeert
  • Regionale cascades: Azië-Pacific-spelers bereiken de CDN 8-12 uur na NA, wat een tweede golf creëert
  • Assetversie-explosie: Elke patch invalideert gecachte objecten, waardoor origin-pulls voor nieuwe hashes nodig zijn

Toen cdnjs migreerde naar Cloudflare's infrastructuur, hadden ze een vergelijkbaar versie-explosieprobleem. Hun npm-achtige versionering betekende dat elke bibliotheekupdate nieuwe cachekeys creëerde, en met 4.200+ bibliotheken die dagelijks werden bijgewerkt, moest het origin-shielding-ontwerp continue cache-churn aan kunnen — niet alleen statische content.

De drie faalmodi

1. Origin-pull-verzadiging

Wanneer je edge-cache mist (nieuwe patch, koude cache, verlopen cache), raakt elk verzoek je originserver. Eén origin met 1 Gbps doorvoer kan ruwweg 1.250 gelijktijdige downloads van 1 MB-assets bedienen. Bij 80.000 gelijktijdige spelers die elk een patch van 2 GB downloaden, heb je origin-capaciteit nodig die de meeste indie-opstellingen simpelweg niet hebben.

2. Cache-stampede

Wanneer je meest aangevraagde asset verloopt uit de edge-cache (TTL-misconfiguratie, purge door deploy), vragen duizenden edge-nodes tegelijk hetzelfde object op bij de origin. Dit is het "thundering herd"-probleem en het laat origins in seconden crashen.

3. Regionale edge-uithongering

Je NA-edge-nodes zijn warm. Je Singapore-edge-node heeft een cache-hitratio van 60% omdat je maar 12.000 APAC-spelers hebt — totdat een YouTuber in Japan je game onder de aandacht brengt en dat aantal 's nachts naar 300.000 springt. De edge-node trekt op grote schaal van de origin, en APAC-spelers ervaren laadtijden van 2-4 seconden terwijl NA-spelers 40ms zien.

Detectiesignalen

# Cloudflare API: check cache hit ratio by region (run every 60 seconds)
curl -s -X POST "https://api.cloudflare.com/client/v4/graphql" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "{
      viewer {
        zones(filter: {zoneTag: \"YOUR_ZONE\"}) {
          httpRequests1hGroups(limit: 24, filter: {date_gt: \"2025-01-01\"}) {
            dimensions { datetime, cacheStatus, clientCountryName }
            sum { requests, bytes }
          }
        }
      }
    }"
  }' | jq '.data.viewer.zones[0].httpRequests1hGroups[] |
    select(.dimensions.cacheStatus == "miss") |
    {region: .dimensions.clientCountryName, misses: .sum.requests}'

Als je cache-missratio in een stabiele periode hoger is dan 8% in welke regio dan ook, ben je één patch verwijderd van een origin-overstroming.


Directe remediëring: wat je nu moet doen

Wanneer de CDN in brand staat, heb je een venster van 15 minuten voordat spelers met review-bombardementen beginnen. Hier is de triagevolgorde.

Stap 1: Activeer origin shielding

De meeste CDN-providers bieden een "origin shield"- of "shielding"-functie — een tussenliggende cachelaag tussen je edge-nodes en je origin. In plaats van dat 200 edge-nodes elk onafhankelijk de origin raken bij een cachemiss, neemt alleen de shield-node contact op met de origin en verdeelt de response.

Configuratievoorbeeld (generieke CDN-API):

{
  "shielding": {
    "enabled": true,
    "shield_region": "us-east-1",
    "fallback_shield_region": "eu-west-1",
    "shield_ttl_override": 86400,
    "pass_on_shield_error": false
  }
}

Deze ene wijziging kan de origin-belasting met 95% verminderen tijdens een cache-stampede. De cdnjs-migratie vertrouwde op vergelijkbare shielding-logica — hun originservers zagen een reductie van miljoenen directe pulls naar een paar duizend shield-geïnitieerde verzoeken per uur.

Stap 2: Verleng asset-TTL's voor statische content

Je 4K-texturen, audiobanken en mesh-bestanden veranderen niet tussen patches. Er is geen reden voor een TTL van 1 uur.

# nginx origin server: aggressive caching for immutable game assets
location /assets/v*/ {
    # Version-prefixed paths mean new versions get new URLs
    # No need to purge — old URLs stay cached forever
    add_header Cache-Control "public, max-age=31536000, immutable";
    add_header CDN-Cache-Control "max-age=31536000";
}

# Short TTL only for manifest files that change each patch
location /manifest.json {
    add_header Cache-Control "public, max-age=60, stale-while-revalidate=300";
}

Het belangrijkste inzicht uit de cdnjs-architectuur: versioneer je assets in het URL-pad, niet met queryreeksen. Veel CDN-nodes behandelen ?v=2 en ?v=3 als dezelfde cachekey. Gebruik in plaats daarvan /assets/v2/texture_pack.bin.

Stap 3: Schakel stale-while-revalidate in

Dit is de meest impactvolle configuratie voor verkeer op de launchedag. Wanneer een gecachte asset verloopt, serveert de CDN de stale-versie aan de verzoekende speler terwijl de nieuwe versie op de achtergrond wordt opgehaald. De speler krijgt een response van 12ms in plaats van 1.200ms.

Cache-Control: public, max-age=3600, stale-while-revalidate=86400

Dit vertelt de CDN: "Deze asset is 1 uur vers. Daarna serveer je tot 24 uur de stale-versie terwijl je op de achtergrond hervalideert."

Voor game-assets die niet veiligheidskritisch zijn (lobby-achtergronden, cosmetische previews, audiostems) is dit veilig en vermindert het de waargenomen latentie dramatisch.

Stap 4: Implementeer circuit-breaker-fallbacks

Als de CDN-origin echt overweldigd is, heeft je gameclient een pad voor geleidelijke degradatie nodig — geen bevroren laadscherm.

// C# Unity: CDN circuit breaker with local fallback
public class AssetLoader
{
    private const int MAX_RETRIES = 3;
    private const int TIMEOUT_MS = 5000;
    private static int _failureCount = 0;
    private static DateTime _circuitOpened = DateTime.MinValue;
    private static readonly TimeSpan CIRCUIT_RESET = TimeSpan.FromMinutes(2);

    public async Task<byte[]> LoadAsset(string assetPath)
    {
        // Circuit breaker: skip CDN if recent failures exceeded threshold
        if (_failureCount >= MAX_RETRIES &&
            DateTime.UtcNow - _circuitOpened < CIRCUIT_RESET)
        {
            Debug.LogWarning($"CDN circuit open — loading {assetPath} from local cache");
            return LoadFromLocalStorage(assetPath);
        }

        try
        {
            using var client = new HttpClient { Timeout = TimeSpan.FromMilliseconds(TIMEOUT_MS) };
            var response = await client.GetAsync($"https://cdn.yourgame.com/{assetPath}");
            response.EnsureSuccessStatusCode();
            _failureCount = 0; // Reset on success
            return await response.Content.ReadAsByteArrayAsync();
        }
        catch (Exception ex)
        {
            _failureCount++;
            if (_failureCount >= MAX_RETRIES)
                _circuitOpened = DateTime.UtcNow;

            Debug.LogWarning($"CDN fetch failed ({_failureCount}/{MAX_RETRIES}): {ex.Message}");
            return LoadFromLocalStorage(assetPath);
        }
    }

    private byte[] LoadFromLocalStorage(string assetPath)
    {
        // Ship a minimal "emergency asset pack" with your game binary
        // This covers the 20 most critical assets: UI, default textures, lobby music
        var localPath = Path.Combine(Application.streamingAssetsPath, "fallback", assetPath);
        return File.Exists(localPath) ? File.ReadAllBytes(localPath) : Array.Empty<byte>();
    }
}

Dit patroon zorgt ervoor dat je game functioneel blijft, zelfs wanneer de CDN volledig down is. Spelers zien misschien een paar minuten texturen met lagere resolutie, maar ze kunnen nog steeds spelen.


Preventie: de meerlaagse cache-architectuur

Remediëring redt je op de launchedag. Architectuur voorkomt dat je het nodig hebt.

Het drielaagse patroon

De cdnjs-migratie naar Cloudflare Workers demonstreerde een cache-architectuur die schaalt naar miljarden verzoeken. Aangepast voor game-assets:

Laag 1 — Edge-cache (CDN-PoPs)

  • Bedient 95-99% van de verzoeken
  • TTL: 365 dagen voor versiebeheerde assets, 60 seconden voor manifests
  • Bevat texturen, meshes, audio, shaders

Laag 2 — Shield/midden-cache

  • Onderschept cachemissen van edge-nodes
  • TTL: zelfde als edge, maar fungeert als origin-proxy
  • Vermindert origin-belasting met 95%+

Laag 3 — Originserver

  • Genereert assets, ondertekent URL's, serveert manifests
  • Beschermd door rate limiting en shielding
  • Zou <0,1% van het totale verkeersvolume moeten zien

Versiebeheerde asset-pipeline

Hier is de workflow voor assetversionering die cache-invalidatiestormen voorkomt:

# Python: asset pipeline that generates cache-safe versioned URLs
import hashlib
import json
import os

def build_asset_manifest(asset_dir: str, cdn_base: str) -> dict:
    """
    Walk asset directory, hash each file, and produce a manifest
    with versioned URLs that CDN edge nodes can cache forever.
    """
    manifest = {"version": "", "assets": {}}

    for root, _, files in os.walk(asset_dir):
        for filename in sorted(files):
            filepath = os.path.join(root, filename)
            relative_path = os.path.relpath(filepath, asset_dir)

            # Content hash — identical files get identical URLs
            with open(filepath, "rb") as f:
                file_hash = hashlib.sha256(f.read()).hexdigest()[:12]

            # Version in the PATH, not query string
            # CDN treats /assets/a3f9b2c1e8d4/texture.bin as a unique object
            versioned_url = f"{cdn_base}/assets/{file_hash}/{relative_path}"

            manifest["assets"][relative_path] = {
                "url": versioned_url,
                "hash": file_hash,
                "size": os.path.getsize(filepath),
            }

    # Manifest version = hash of the entire asset set
    all_hashes = "".join(
        a["hash"] for a in sorted(manifest["assets"].values(), key=lambda x: x["url"])
    )
    manifest["version"] = hashlib.sha256(all_hashes.encode()).hexdigest()[:16]

    return manifest


# Usage
manifest = build_asset_manifest("./build/assets", "https://cdn.yourgame.com")
with open("./build/manifest.json", "w") as f:
    json.dump(manifest, f, indent=2)

print(f"Manifest version: {manifest['version']}")
print(f"Total assets: {len(manifest['assets'])}")
# Output:
# Manifest version: a8f3e1c92b4d7061
# Total assets: 2,847

Met deze aanpak:

  • Oude assets worden nooit gepurged. Ze blijven voor onbepaalde tijd gecacht aan de edge omdat ze unieke URL's hebben.
  • Nieuwe assets krijgen nieuwe URL's. De CDN-cachet ze automatisch bij het eerste verzoek.
  • Het enige bestand dat verandert is de manifest. Een klein JSON-bestand met een TTL van 60 seconden.

Dit is precies hoe cdnjs bibliotheekversionering op schaal aanpakt. Elke bibliotheekversie krijgt een uniek URL-pad, zodat de CDN nooit purge-operaties nodig heeft — de duurste en meest foutgevoelige CDN-operatie die er bestaat.

Dit architectuurpatroon is vooral belangrijk als je dedicated servers draait die configuratiegegevens moeten serveren naast gamelogica. Zoals we behandelden in onze gids over hoe je Unreal Engine dedicated server asset stripping onder de knie krijgt, is het scheiden van statische assets van serverkritieke data een fundamentele optimalisatie die op schaal rendeert.


Geografische spreiding: de regionale cascade oplossen

De cdnjs-migratie onthulde dat het aantal edge-nodes minder belangrijk is dan intelligente routing. 300 PoPs hebben betekent niets als de routinglogica APAC-verzoeken bij een US-origin op cachemiss aflevert.

Slimme origin-selectie

{
  "origin_rules": [
    {
      "name": "us-primary",
      "origin_server": "origin-us.yourgame.com",
      "regions": ["NA", "SA"],
      "health_check": "/health",
      "failover_origin": "origin-eu.yourgame.com"
    },
    {
      "name": "eu-primary",
      "origin_server": "origin-eu.yourgame.com",
      "regions": ["EU", "AF"],
      "health_check": "/health",
      "failover_origin": "origin-us.yourgame.com"
    },
    {
      "name": "apac-primary",
      "origin_server": "origin-apac.yourgame.com",
      "regions": ["AS", "OC"],
      "health_check": "/health",
      "failover_origin": "origin-us.yourgame.com"
    }
  ]
}

Regionale originservers kosten $20-40 per maand per stuk bij de grote cloudproviders. Drie regionale origins kosten minder dan één incident waarbij je NA-origin 4 uur lang APAC-verkeer bedient met verminderde prestaties — en de verloren spelers die daarbij horen.

Dit soort multi-regio-failoverarchitectuur weerspiegelt wat we bespreken in onze analyse van het ontwerpen van zero-waste servers met hibernationstrategieën — het principe van niet betalen voor inactieve infrastructuur terwijl je toch klaar bent voor schaal.


Best practices voor het schalen van CDN-game-assets

1. Versioneer assets in URL-paden, niet in queryreeksen. /assets/{hash}/texture.bin garandeert cache-uniekheid. ?v=2 doet dat niet — veel CDN-nodes strippen queryparameters uit cachekeys, waardoor je verouderde content of kapotte caches krijgt.

2. Scheid je manifest-TTL van je asset-TTL. Manifestbestanden moeten een TTL van 30-60 seconden hebben met stale-while-revalidate. Assetbestanden moeten een TTL van 1 jaar hebben met immutable. Dit onderscheid is het verschil tussen een soepele patchuitrol en een cache-stampede.

3. Lever een fallback-assetpakket mee met je gamebinary. De 50-100 meest kritieke assets (UI, standaardskin, lobby-omgeving) moeten in je game-installatie zitten als een noodpakket van 200-500 MB. Je circuit-breakerlogica valt hierop terug wanneer de CDN onbereikbaar is.

4. Monitor de cache-hitratio per regio, niet globaal. Een wereldwijde hitratio van 97% kan een hitratio van 72% in Zuidoost-Azië maskeren. Per-regio-monitoring laat je regionale edge-uithongering opmerken voordat het een door spelers gemeld incident wordt.

5. Load-test je CDN vóór de launch, niet tijdens. Gebruik tools zoals k6, Locust of Vegeta om je verwachte lanceerdag-verkeerspatroon te simuleren tegen je CDN-endpoint. Een test van 10 minuten met 50.000 virtuele gebruikers die je manifest + top 20 assets raken, onthult verkeerd geconfigureerde TTL's, ontbrekende shielding en origin-flessenhalzen voordat echte spelers dat doen.

# k6: simulate 50,000 concurrent players hitting the asset manifest
cat <<'EOF' > cdn_load_test.js
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '2m', target: 10000 },  // Ramp to 10K VUs
    { duration: '3m', target: 50000 },  // Spike to 50K VUs
    { duration: '5m', target: 50000 },  // Sustain
    { duration: '2m', target: 0 },      // Ramp down
  ],
  thresholds: {
    http_req_duration: ['p(95)<200'],   // 95th percentile under 200ms
    http_req_failed: ['rate<0.01'],     // Less than 1% errors
  },
};

export default function () {
  const manifestRes = http.get('https://cdn.yourgame.com/manifest.json');
  check(manifestRes, {
    'manifest status 200': (r) => r.status === 200,
    'manifest under 100ms': (r) => r.timings.duration < 100,
    'cache HIT': (r) => r.headers['Cf-Cache-Status'] === 'HIT',
  });

  // Simulate a player downloading 5 random assets
  for (let i = 0; i < 5; i++) {
    const assetPath = `assets/placeholder_${Math.floor(Math.random() * 100)}/mesh.bin`;
    const assetRes = http.get(`https://cdn.yourgame.com/${assetPath}`);
    check(assetRes, {
      'asset under 500ms': (r) => r.timings.duration < 500,
    });
  }

  sleep(1);
}
EOF

k6 run cdn_load_test.js

Wanneer bouw je het zelf versus gebruik je een platform

Het bouwen van de volledige meerlaagse cache-architectuur die hierboven is beschreven, is zeker haalbaar voor een team met toegewijde infrastructuuringenieurs. De componenten zijn goed gedocumenteerd en CDN-providers bieden de ruwe primitieven.

Maar als je team uit drie ontwikkelaars bestaat die een game uitbrengen, betekent 4-6 weken besteden aan het bouwen van origin shielding, regionale failover, assetversioneringspijplijnen en circuit-breakerlogica in je client dat je 4-6 weken niet aan gameplay besteedt. horizOn handelt assetleveringsinfrastructuur af als onderdeel van zijn backendstack, waardoor je dezelfde multi-regio-caching en automatische failover krijgt zonder de operationele overhead. Je uploadt assets; het platform regelt versionering, edge-distributie en health-monitoring kant-en-klaar.

De architectuurprincipes in dit artikel blijven cruciaal, ongeacht je infrastructuurkeuze. Begrijpen waarom versiebeheerde URL-paden ertoe doen, waarom stale-while-revalidate stampedes voorkomt, en waarom regionale origins de latentie verminderen, stelt je in staat om geïnformeerde beslissingen te nemen — of je nu Cloudflare Workers handmatig configureert of een beheerde backenddienst evalueert.


Volgende stap: voer de load-test uit vóór je volgende patch

Kies je volgende patchdatum. Twee weken van tevoren voer je het bovenstaande k6-script uit tegen je CDN-endpoint. Als je p95-latentie hoger is dan 200ms bij gesimuleerde launchschaal, heb je tijd om het te repareren. Als je ontdekt dat je cache-hitratio tijdens de stabiele fase onder de 90% zakt, activeer dan origin shielding en verleng je asset-TTL's.

Het verschil tussen een soepele launch en een ramp op launchedag zit zelden in de gamecode. Het zit in de infrastructuur die de 2 GB aan assets serveert die elke speler in de eerste vijf minuten downloadt. Zorg dat dat klopt, en de rest is gameplay.