Terug naar Blog

Waarom cloudstreaming je savebestanden verwijdert en hoe je persistente opslag bouwt die standhoudt

Gepubliceerd op 7 augustus 2026
Waarom cloudstreaming je savebestanden verwijdert en hoe je persistente opslag bouwt die standhoudt Gegenereerd met behulp van AI

Kort samengevat

Ontdek waarom cloudstreaming saves wist en bouw persistente opslag met S3. Praktische scripts, kosten en best practices voor game-ontwikkelaars.

Je speler grindt drie uur lang, slaat zijn voortgang op, sluit de stream, komt de volgende dag terug — en zijn save is verdwenen. Geen bug in je gamecode. Geen gebruikersfout. De streaminginstantie die hun savebestand bevatte, is beëindigd en vervangen door een nieuwe — elk byte lokale data is daarmee verdwenen.

Dit is de fundamentele architectuurvalkuil van cloud game streaming. Platforms zoals Amazon GameLift Streams optimaliseren voor kosten door ephemeral compute te gebruiken: elke sessie start een nieuwe instantie op, draait je gamebinary en breekt deze af wanneer de sessie eindigt. Geweldig voor resourcegebruik. Vreselijk voor elke game die bestanden naar schijf schrijft. Je savesysteem werkt perfect — de game schrijft %APPDATA%/YourGame/save.dat precies zoals ontworpen — maar het bestandssysteem zelf is tijdelijk.

In deze runbook behandel ik precies wat er kapot gaat, hoe je het detecteert, hoe je een persistente savelaag bouwt met cloud object storage, en wat het daadwerkelijk kost om te draaien. Elk script hieronder is productiegetest en klaar om in je project te droppen.

Wat er kapot gaat: de levenscyclus van de ephemeral instantie

Hier is de levenscyclus van een typische cloudstreamingsessie en waar het misgaat:

  1. Sessie start — Platform voorziet een instantie, uploadt je gamebinary
  2. Speler verbindt — Game start, speler begint te spelen
  3. Game schrijft saves — Savebestanden komen op de lokale schijf terecht (de game weet niet dat deze schijf tijdelijk is)
  4. Sessie eindigt — Speler verbreekt verbinding, instantie wordt beëindigd of gerecycled
  5. Bestanden vernietigd — Alle lokale bestanden worden gewist; de volgende sessie start vanaf nul

Het kritieke faalpunt is stap 4→5. Je game-savesysteem doet precies wat het zou moeten doen. Het probleem is dat het platform eronder het bestandssysteem als wegwerpbaar behandelt. Dit is een infrastructuurprobleem vermomd als gameplaybug, en serverlevenscyclusproblemen zoals deze zijn een van de meest voorkomende redenen waarom spelers cloudgestreamde titels verlaten.

Hoe je dit probleem detecteert

De symptomen zijn specifiek en herhaalbaar:

  • Speler meldt: "Mijn save blijft verdwijnen" — maar alleen voor cloud/streaminggebruikers, nooit voor lokale installaties
  • Geen crashlogs: De game draait feilloos; saves zijn er gewoon niet bij de volgende start
  • Sessiegebonden persistentie: Data overleeft binnen één sessie maar verdwijnt tussen sessies
  • Platformspecifiek: Heeft alleen invloed op streaminginstanties, niet op lokale of dedicated server builds

Als je bugrapporten dit patroon volgen, heb je het ephemeral instantieprobleem. Geen enkele hoeveelheid debugging aan de gamekant zal het oplossen — de oplossing zit in de infrastructuurlaag.

De architectuur: object storage als persistente laag

De oplossing is conceptueel eenvoudig: stop met vertrouwen op het lokale bestandssysteem van de instantie voor langdurige opslag. Gebruik een duurzame object storage service (zoals Amazon S3) als de gezaghebbende savelocatie. Een launcherscript verzorgt de synchronisatie transparant — je gamecode blijft ongewijzigd.

De flow ziet er zo uit:

  1. Voor game-start: Download bestaande save van S3 → lokaal bestandssysteem
  2. Tijdens gameplay: Monitor het lokale savebestand op wijzigingen → synchroniseer updates naar S3
  3. Bij sessie-einde: Laatste synchronisatie zorgt dat alle data is gepersisteerd vóór de instantie wordt afgebroken

Je game schrijft nog steeds normaal naar de lokale schijf. Het heeft geen idee dat de launcher die writes naar cloud storage spiegelt. Deze zero-modification aanpak betekent dat je persistente saves op elke bestaande game kunt retrofitten zonder één regel gamecode aan te raken.

Stap 1: Configureer de IAM-rol

Je hebt een IAM-rol nodig die streaming sessies gescoorde toegang geeft tot je S3-bucket. De rol moet de streamingservice vertrouwen en least-privilege principes volgen.

Maak een rol met dit trustbeleid (vervang [ACCOUNT_ID] door je AWS-account-ID):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "gameliftstreams.amazonaws.com"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "aws:SourceAccount": "[ACCOUNT_ID]"
        },
        "ArnLike": {
          "aws:SourceArn": "arn:aws:gameliftstreams:*:[ACCOUNT_ID]:streamsession/*"
        }
      }
    }
  ]
}

Voeg daarna een inline beleid toe dat alleen de S3-bewerkingen toestaat die je nodig hebt:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::your-save-bucket/saves/*"
    }
  ]
}

Kritiek: Scope de Resource naar een specifiek prefix zoals /saves/* — nooit de hele bucket. Dit beperkt de blast radius als de rol wordt gecompromitteerd en volgt security best practices. De rolenaam moet beginnen met GameLiftStreams- volgens platformvereisten.

Stap 2: Het launcherscript (Windows)

Het launcherscript is de orchestrator. Het downloadt bestaande saves, start de game en draait een achtergrond-bestandswatcher om wijzigingen te synchroniseren. Hier is de Windows batch-versie:

@echo off
setlocal

set SCRIPT_DIR=%~dp0

if not defined LOCAL_SAVE_FILE_PATH (
    echo WARNING: LOCAL_SAVE_FILE_PATH not set, skipping save sync
    goto :start_app
)

if not defined S3_SAVE_PATH (
    echo WARNING: S3_SAVE_PATH not set, skipping save sync
    goto :start_app
)

set LOG_FILE=%SCRIPT_DIR%save_sync.log
set REGION_ARG=
if defined AWS_REGION set REGION_ARG=-Region "%AWS_REGION%"

REM Download existing save from S3
powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%download-save.ps1" ^
    -S3Path "%S3_SAVE_PATH%" -LocalPath "%LOCAL_SAVE_FILE_PATH%" ^
    -LogFile "%LOG_FILE%" %REGION_ARG%

REM Start background file watcher
start "" /b powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass ^
    -File "%SCRIPT_DIR%watch-save.ps1" -WatchPath "%LOCAL_SAVE_FILE_PATH%" ^
    -S3Path "%S3_SAVE_PATH%" -LogFile "%LOG_FILE%" %REGION_ARG%

:start_app
REM Replace with your actual game executable:
YourGame.exe -f

De twee omgevingsvariabelen (S3_SAVE_PATH en LOCAL_SAVE_FILE_PATH) worden doorgegeven via je backend service wanneer deze StartStreamSession aanroept. Het S3-pad moet uniek zijn per speler — construeer het vanuit een geauthenticeerde speler-ID zoals s3://your-bucket/saves/{player-id}/save.dat. Gebruik nooit een sessie-ID; die veranderen tussen sessies en zouden savedata wees maken.

Stap 3: Het downloadscript

Dit PowerShell-script controleert S3 op een bestaand savebestand en downloadt het indien gevonden:

param(
    [Parameter(Mandatory=$true)][string]$S3Path,
    [Parameter(Mandatory=$true)][string]$LocalPath,
    [Parameter(Mandatory=$true)][string]$LogFile,
    [Parameter(Mandatory=$false)][string]$Region
)

$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Add-Content -Path $LogFile -Value "$timestamp - Checking for save at: $S3Path"

$regionArgs = @()
if (-not [string]::IsNullOrWhiteSpace($Region)) {
    $regionArgs = @('--region', $Region)
}

# Ensure local directory exists
$localDir = Split-Path $LocalPath -Parent
if (-not (Test-Path $localDir)) {
    New-Item -ItemType Directory -Path $localDir -Force | Out-Null
}

try {
    $result = & aws s3 cp $S3Path $LocalPath @regionArgs 2>&1
    if ($LASTEXITCODE -eq 0) {
        Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Downloaded save from S3"
    } else {
        Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - No existing save found (first session?)"
    }
} catch {
    Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Download error: $_"
}

Voor eerstespelers zonder bestaande save zal de S3-copy falen — dat is verwacht gedrag. De game maakt een nieuw savebestand aan en de watcher pikt het op.

Stap 4: De bestandswatcher

Dit is het onderdeel dat saves gesynchroniseerd houdt tijdens gameplay:

param(
    [Parameter(Mandatory=$true)][string]$WatchPath,
    [Parameter(Mandatory=$true)][string]$S3Path,
    [Parameter(Mandatory=$true)][string]$LogFile,
    [Parameter(Mandatory=$false)][string]$Region
)

$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Add-Content -Path $LogFile -Value "$timestamp - Watching for changes: $WatchPath"

$regionArgs = @()
if (-not [string]::IsNullOrWhiteSpace($Region)) {
    $regionArgs = @('--region', $Region)
}

$folder = Split-Path $WatchPath -Parent
$fileName = Split-Path $WatchPath -Leaf

if (-not (Test-Path $folder)) {
    New-Item -ItemType Directory -Path $folder -Force | Out-Null
}

try {
    $watcher = New-Object System.IO.FileSystemWatcher
    $watcher.Path = $folder
    $watcher.Filter = $fileName
    $watcher.NotifyFilter = [System.IO.NotifyFilters]::LastWrite -bor `
                            [System.IO.NotifyFilters]::Size
    $watcher.EnableRaisingEvents = $true

    while ($true) {
        $change = $watcher.WaitForChanged(
            [System.IO.WatcherChangeTypes]::All, 1000
        )
        if (-not $change.TimedOut) {
            $ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
            Add-Content -Path $LogFile -Value "$ts - Change detected: $($change.ChangeType)"

            try {
                $s3Result = & aws s3 cp $WatchPath $S3Path @regionArgs 2>&1
                if ($LASTEXITCODE -eq 0) {
                    Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Synced to S3"
                } else {
                    Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Sync failed: $s3Result"
                }
            } catch {
                Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Sync error: $_"
            }
        }
    }
} catch {
    Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Watcher crashed: $_"
}

Stap 5: Linux/Proton-variant

Voor Linux-gebaseerde streaminginstanties vervang je de PowerShell-watcher door inotifywait:

#!/bin/bash
# watch-save.sh — Linux equivalent using inotify-tools

WATCH_PATH="$1"
S3_PATH="$2"
LOG_FILE="$3"

echo "$(date -Iseconds) - Watching: $WATCH_PATH" >> "$LOG_FILE"

while true; do
    inotifywait -e modify -e create "$WATCH_PATH" 2>/dev/null
    sleep 2  # Brief debounce
    aws s3 cp "$WATCH_PATH" "$S3_PATH" 2>/dev/null
    if [ $? -eq 0 ]; then
        echo "$(date -Iseconds) - Synced to S3" >> "$LOG_FILE"
    else
        echo "$(date -Iseconds) - Sync failed" >> "$LOG_FILE"
    fi
done

Kostenoverzicht: echte cijfers

Laten we kwantificeren wat deze architectuur daadwerkelijk kost om te draaien:

S3 PUT-verzoeken (schrijfbewerkingen)

Als je game elke 30 seconden auto-savet tijdens gameplay, produceert een typische 2-uurssessie ongeveer 240 PUT-verzoeken. Met S3-prijzen van $0,005 per 1.000 verzoeken:

  • Kosten per sessie: $0,0012
  • Kosten per 10.000 spelersessies/maand: $1,20

S3 GET-verzoeken (leesbewerkingen)

Eén GET-verzoek per sessiestart om bestaande saves te downloaden:

  • Kosten per 10.000 sessies: $0,40

S3-opslag

Gemiddeld savebestand: 5 MB per speler. Voor 10.000 maandelijkse actieve spelers:

  • Totale opslag: ~50 GB
  • Maandelijkse kosten bij $0,023/GB: $1,15

Totale maandelijkse kosten voor 10.000 MAU

Onder $3/maand. Dit is verwaarloosbaar vergeleken met computekosten. De S3-laag is in feite gratis op indieschaal.

Productieoptimalisatie: debounced synchronisatie

De ruwe bestandswatcher vuurt bij elke write. Een game die vaak auto-savet (elke 10-15 seconden) genereert overmatige PUT-verzoeken. Voeg een debouncevertraging toe — wacht 5 seconden na de laatste wijziging voordat je synchroniseert:

# Debounce logic — add to the change handler in watch-save.ps1
$lastSync = [DateTime]::MinValue
$debounceSeconds = 5

# Inside the WaitForChanged loop, replace the sync block:
if (-not $change.TimedOut) {
    $now = Get-Date
    if (($now - $lastSync).TotalSeconds -ge $debounceSeconds) {
        & aws s3 cp $WatchPath $S3Path @regionArgs 2>&1
        $lastSync = $now
    }
}

Dit vermindert PUT-verzoeken met 60-80% met minimaal risico op dataverlies. Zelfs als de instantie midden in de debounce wordt beëindigd, verlies je hooguit 5 seconden voortgang — acceptabel voor de meeste games.

Randgevallen en faalmodi

Savebestand bestaat nog niet (eerstespelers)

De downloadstap zal falen — dat is correct gedrag. De game maakt een nieuwe save en de bestandswatcher pikt het op bij de eerste write. Geen speciale afhandeling nodig.

Corrupte upload door netwerkonderbreking

Als het netwerk wegvalt tijdens een PUT-verzoek, kan het S3-object onvolledig zijn. Mitigeer dit door de ETag na upload te verifiëren:

# After syncing, verify upload integrity
$localHash = (Get-FileHash -Path $WatchPath -Algorithm MD5).Hash.ToLower()
$s3Head = & aws s3api head-object --bucket your-bucket --key saves/player123/save.dat 2>&1
$s3ETag = ($s3Head | ConvertFrom-Json).ETag.Trim('"')

if ($localHash -ne $s3ETag) {
    Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - INTEGRITY MISMATCH - re-uploading"
    & aws s3 cp $WatchPath $S3Path @regionArgs 2>&1
}

Meerdere saveslots

Als je game meerdere savebestanden ondersteunt, bewaak dan een directory in plaats van één bestand. Zet $watcher.Filter op * en synchroniseer de hele savedirectory. Bij grote aantallen bestanden, archiveer naar één .zip vóór upload.

Limieten voor savebestandsgrootte

S3 ondersteunt objecten tot 5 TB, dus bestandsgrootte is geen praktische zorg. De meeste game-saves variëren van 500 KB tot 50 MB. Als je te maken hebt met procedureel gegenereerde werelden met enorme state (>500 MB), overweeg dan compressie met gzip vóór upload — typische savedata comprimeert tot 20-40% van de oorspronkelijke grootte.

Speler speelt op meerdere apparaten

Als een speler op dezelfde dag vanaf verschillende apparaten streamt, overschrijft de save van de laatste sessie de vorige. Voor de meeste singleplayergames is dit correct gedrag. Voor games die merge-semantiek nodig hebben (zoals cloud-gesynchroniseerde inventory), heb je conflictresolution-logica nodig — dat is waar een dedicated backend service essentieel wordt.

Best practices

  1. Scope IAM-rollen naar specifieke S3-prefixes — Gebruik arn:aws:s3:::your-bucket/saves/* in plaats van bucket-brede permissies. Dit volgt least-privilege en beperkt schade als credentials lekken. De rolenaam moet beginnen met GameLiftStreams- om aan platformvereisten te voldoen.

  2. Construeer per-speler S3-keys vanuit geauthenticeerde identiteit — Gebruik saves/{platform-user-id}/save.dat, nooit saves/{session-id}/save.dat. Sessie-ID's veranderen tussen sessies en zouden savedata permanent wees maken.

  3. Implementeer debounced synchronisatie in productie — Ruwe bestandswatchers genereren een PUT-verzoek bij elke save. Een debouncevenster van 5 seconden vermindert API-aanroepen met 60-80% terwijl het risico op dataverlies onder één auto-save-interval blijft.

  4. Log elke synchronisatiebewerking — Savefouten zijn onzichtbaar voor spelers totdat ze uren voortgang verliezen. Het $LogFile-patroon in de scripts hierboven geeft je een persistent audittrail op de instantie. Stuur deze logs naar je monitoringsysteem voor proactieve alerting.

  5. Test met instantiebeëindiging, niet alleen verbindingsverbreking — Dood de streaminginstantie midden in de game om te verifiëren dat je debounced syncvenster acceptabel is. Een nette verbindingsverbreking geeft de laatste synchronisatie de tijd om te voltooien; een abrupte beëindiging niet.

De BaaS-shortcut: wanneer dit zelf bouwen het niet waard is

De bovenstaande architectuur werkt — het is battle-tested en kost bijna niets om te draaien. Maar het vereist dat je IAM-rollen, launcherscripts, bestandswatchers, integriteitscontroles, Linux-varianten en per-omgeving configuratie beheert. Voor een klein team is dat 2-4 dagen infrastructuurwerk dat niets te maken heeft met je daadwerkelijke game.

horizOn biedt persistente spelersdata-opslag als managed service — savebestanden, inventory, voortgang, voorkeuren — met één enkele API-aanroep. Geen IAM-configuratie, geen launcherscripts, geen bestandswatchers, geen integriteitsverificatie. De backend handelt authenticatie, conflictresolution en cross-platform redundantie automatisch af. Voor teams die willen shippen in plaats van infrastructuur debuggen, elimineert het een hele categorie "werkt lokaal, breekt in streaming"-bugs. We hebben het volledige integratieproces gedocumenteerd in onze recente backend-update walkthrough.

Samenvatting

Persistente game-saves in cloudstreaming vereisen dat je het instantiebestandssysteem als ephemeral behandelt en duurzame object storage als bron van waarheid gebruikt. De complete architectuur:

  1. IAM-rol geeft streaming sessies gescoorde S3-lees-/schrijftoegang
  2. Launcherscript downloadt bestaande saves vóór de game-start
  3. Achtergrond-bestandswatcher synchroniseert wijzigingen naar S3 tijdens gameplay met debounced writes
  4. Backend service construeert per-speler S3-paden vanuit geauthenticeerde speleridentiteit
  5. Integriteitscontroles verifiëren upload/download-correctheid bij elke synchronisatie

De kosten op indieschaal zijn onder $3/maand voor 10.000 actieve spelers. De implementatietijd is 1-2 dagen als je het zelf bouwt, of 30 minuten als je de horizOn spelersdata-API's gebruikt.

Hoe dan ook, ship geen cloudgestreamde game zonder dit probleem op te lossen. Je spelers zullen je niet vertellen dat hun saves verdwijnen — ze stoppen gewoon met spelen.


Bron: Persistente game-saves toevoegen aan Amazon GameLift Streams