Por Que o Cloud Streaming Apaga Seus Saves e Como Criar Armazenamento Persistente que Sobrevive
Em resumo
Descubra por que o cloud streaming apaga saves e aprenda a criar armazenamento persistente com S3, scripts de launcher e file watchers em produção.
Seu jogador passa três horas grindando, salva o progresso, fecha o stream, volta no dia seguinte — e o save sumiu. Não é bug no código do seu jogo. Não é erro do usuário. A instância de streaming que guardava o save foi encerrada e substituída por uma nova, levando cada byte de dados locais junto.
Essa é a armadilha arquitetural fundamental do cloud game streaming. Plataformas como Amazon GameLift Streams otimizam custo usando compute efêmero: cada sessão sobe uma instância nova, executa o binário do jogo e a derruba quando a sessão termina. Ótimo para utilização de recursos. Péssimo para qualquer jogo que grava arquivos em disco. Seu sistema de save funciona perfeitamente — o jogo grava %APPDATA%/YourGame/save.dat exatamente como projetado — mas o filesystem em si é temporário.
Neste runbook, vou cobrir exatamente o que quebra, como detectar, como construir uma camada de save persistente usando object storage na nuvem, e quanto custa rodar na prática. Cada script abaixo é testado em produção e pronto para ser integrado ao seu projeto.
O Que Quebra: O Ciclo de Vida da Instância Efêmera
Aqui está o ciclo de vida de uma sessão típica de cloud streaming e onde ela falha:
- Sessão inicia — A plataforma provisiona uma instância e envia o binário do jogo
- Jogador conecta — O jogo abre, o jogador começa a jogar
- Jogo grava saves — Os arquivos de save vão para o disco local (o jogo não sabe que esse disco é temporário)
- Sessão termina — O jogador desconecta, a instância é encerrada ou reciclada
- Arquivos destruídos — Todos os arquivos locais são apagados; a próxima sessão começa do zero
O ponto crítico de falha é o passo 4→5. O sistema de save do seu jogo está fazendo exatamente o que deveria. O problema é que a plataforma por baixo trata o filesystem como descartável. Isso é um problema de infraestrutura disfarçado de bug de gameplay, e problemas de ciclo de vida de servidor como este são uma das razões mais comuns pelas quais jogadores abandonam títulos em cloud streaming.
Como Detectar Esse Problema
Os sintomas são específicos e repetíveis:
- Relato do jogador: "Meu save fica sumindo" — mas só para usuários de cloud/streaming, nunca em instalações locais
- Sem crash logs: O jogo roda perfeitamente; os saves simplesmente não estão lá no próximo launch
- Persistência limitada à sessão: Os dados sobrevivem dentro de uma sessão, mas desaparecem entre sessões
- Específico da plataforma: Só afeta instâncias de streaming, não builds locais ou de dedicated server
Se seus bug reports seguem esse padrão, você tem o problema da instância efêmera. Nenhuma quantidade de debug no lado do jogo vai resolver — a solução vive na camada de infraestrutura.
A Arquitetura: Object Storage como Camada Persistente
A correção é conceitualmente simples: pare de depender do filesystem local da instância para armazenamento de longo prazo. Use um serviço de object storage durável (como Amazon S3) como local autoritativo dos saves. Um script de launcher cuida da sincronização de forma transparente — o código do seu jogo permanece inalterado.
O fluxo é assim:
- Antes do launch do jogo: Baixar save existente do S3 → filesystem local
- Durante o gameplay: Monitorar o arquivo de save local para mudanças → sincronizar atualizações para o S3
- No fim da sessão: Sincronização final garante que todos os dados sejam persistidos antes do teardown da instância
Seu jogo continua gravando no disco local normalmente. Ele não tem ideia de que o launcher está espelhando essas gravações para o cloud storage. Essa abordagem de zero modificação significa que você pode adaptar saves persistentes a qualquer jogo existente sem tocar em uma linha do código do jogo.
Passo 1: Configurar a IAM Role
Você precisa de uma IAM role que conceda às sessões de streaming acesso escopado ao seu bucket S3. A role deve confiar no serviço de streaming e seguir princípios de privilégio mínimo.
Crie uma role com esta trust policy (substitua [ACCOUNT_ID] pelo seu ID de conta AWS):
{
"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/*"
}
}
}
]
}
Depois anexe uma inline policy concedendo apenas as operações S3 que você precisa:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::your-save-bucket/saves/*"
}
]
}
Crítico: Escope o Resource para um prefixo específico como /saves/* — nunca o bucket inteiro. Isso limita o raio de impacto se a role for comprometida e segue as melhores práticas de segurança. O nome da role deve começar com GameLiftStreams- conforme requisitos da plataforma.
Passo 2: O Script de Launcher (Windows)
O script de launcher é o orquestrador. Ele baixa os saves existentes, inicia o jogo e roda um file watcher em background para sincronizar mudanças. Aqui está a versão em batch para Windows:
@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
As duas variáveis de ambiente (S3_SAVE_PATH e LOCAL_SAVE_FILE_PATH) são passadas pelo seu backend service quando ele chama StartStreamSession. O caminho S3 deve ser único por jogador — construa-o a partir de um player ID autenticado como s3://your-bucket/saves/{player-id}/save.dat. Nunca use um session ID; eles mudam entre sessões e orfanariam os dados de save.
Passo 3: O Script de Download
Este script PowerShell verifica no S3 se existe um arquivo de save e o baixa se encontrado:
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: $_"
}
Para jogadores de primeira viagem sem save existente, a cópia S3 vai falhar — isso é esperado. O jogo criará um novo arquivo de save, e o watcher vai capturá-lo.
Passo 4: O File Watcher
Este é o componente que mantém os saves sincronizados durante o 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: $_"
}
Passo 5: Variante Linux/Proton
Para instâncias de streaming baseadas em Linux, substitua o watcher PowerShell por 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
Detalhamento de Custos: Números Reais
Vamos quantificar quanto essa arquitetura custa para rodar:
Requisições PUT no S3 (Operações de Escrita)
Se o seu jogo faz auto-save a cada 30 segundos durante o gameplay, uma sessão típica de 2 horas produz aproximadamente 240 requisições PUT. Com o preço do S3 em $0,005 por 1.000 requisições:
- Custo por sessão: $0,0012
- Custo por 10.000 player-sessions/mês: $1,20
Requisições GET no S3 (Operações de Leitura)
Uma requisição GET por início de sessão para baixar os saves existentes:
- Custo por 10.000 sessões: $0,40
Armazenamento no S3
Tamanho médio do save: 5 MB por jogador. Para 10.000 jogadores ativos mensais:
- Armazenamento total: ~50 GB
- Custo mensal a $0,023/GB: $1,15
Custo Mensal Total para 10.000 MAU
Menos de $3/mês. Isso é desprezível comparado aos custos de compute. A camada S3 é essencialmente grátis em escala indie.
Otimização em Produção: Sincronização com Debounce
O file watcher bruto dispara a cada escrita. Um jogo que faz auto-save com frequência (a cada 10-15 segundos) vai gerar requisições PUT excessivas. Adicione um atraso de debounce — espere 5 segundos após a última mudança antes de sincronizar:
# 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
}
}
Isso reduz as requisições PUT em 60-80% com risco mínimo de perda de dados. Mesmo se a instância for encerrada no meio do debounce, você perde no máximo 5 segundos de progresso — aceitável para a maioria dos jogos.
Casos de Borda e Modos de Falha
Arquivo de Save Não Existe Ainda (Jogadores de Primeira Viagem)
O passo de download vai falhar — esse é o comportamento correto. O jogo cria um novo save, e o file watcher o captura na primeira escrita. Nenhum tratamento especial é necessário.
Upload Corrompido por Interrupção de Rede
Se a rede cair durante uma requisição PUT, o objeto S3 pode ficar incompleto. Mitigue isso verificando o ETag após o upload:
# 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
}
Múltiplos Slots de Save
Se o seu jogo suporta múltiplos arquivos de save, monitore um diretório em vez de um único arquivo. Defina $watcher.Filter para * e sincronize o diretório inteiro de saves. Para um grande número de arquivos, arquive em um único .zip antes do upload.
Limites de Tamanho do Arquivo de Save
O S3 suporta objetos de até 5 TB, então o tamanho do arquivo não é uma preocupação prática. A maioria dos saves de jogos varia de 500 KB a 50 MB. Se você está lidando com mundos gerados proceduralmente com estado massivo (>500 MB), considere comprimir com gzip antes do upload — dados de save típicos comprimem para 20-40% do tamanho original.
Jogador Joga em Múltiplos Dispositivos
Se um jogador faz stream de dispositivos diferentes no mesmo dia, o save da última sessão sobrescreve o anterior. Para a maioria dos jogos single-player, esse é o comportamento correto. Para jogos que precisam de semântica de merge (como inventário sincronizado em nuvem), você vai precisar de lógica de resolução de conflitos — que é onde um backend service dedicado se torna essencial.
Melhores Práticas
Escopar IAM roles a prefixos S3 específicos — Use
arn:aws:s3:::your-bucket/saves/*em vez de permissões no bucket inteiro. Isso segue o princípio do privilégio mínimo e limita danos se credenciais vazarem. O nome da role deve começar comGameLiftStreams-para satisfazer os requisitos da plataforma.Construir chaves S3 por jogador a partir de identidade autenticada — Use
saves/{platform-user-id}/save.dat, nuncasaves/{session-id}/save.dat. Session IDs mudam entre sessões e orfanariam os dados de save permanentemente.Implementar sincronização com debounce em produção — File watchers brutos geram uma requisição PUT a cada save. Uma janela de debounce de 5 segundos corta as chamadas de API em 60-80% mantendo o risco de perda de dados abaixo de um intervalo de auto-save.
Registrar cada operação de sincronização em log — Falhas de save são invisíveis para os jogadores até que eles percam horas de progresso. O padrão
$LogFilenos scripts acima dá a você uma trilha de auditoria persistente na instância. Envie esses logs para seu sistema de monitoramento para alertas proativos.Testar com encerramento de instância, não apenas desconexão — Mate a instância de streaming no meio do jogo para verificar se sua janela de debounce é aceitável. Uma desconexão graciosa dá tempo para a sincronização final completar; um encerramento abrupto não.
O Atalho BaaS: Quando Construir Isso Você Mesmo Não Vale a Pena
A arquitetura acima funciona — é testada em batalha e custa quase nada para rodar. Mas exige que você gerencie IAM roles, scripts de launcher, file watchers, verificações de integridade, variantes Linux e configuração por ambiente. Para um time pequeno, isso são 2-4 dias de trabalho de infraestrutura que não tem nada a ver com o seu jogo de verdade.
horizOn fornece armazenamento persistente de dados do jogador como um serviço gerenciado — arquivos de save, inventário, progresso, preferências — com uma única chamada de API. Sem configuração de IAM, sem scripts de launcher, sem file watchers, sem verificação de integridade. O backend cuida de autenticação, resolução de conflitos e redundância cross-platform automaticamente. Para times que querem lançar em vez de depurar infraestrutura, isso elimina uma categoria inteira de bugs "funciona local, quebra no streaming". Documentamos o processo completo de integração no nosso walkthrough de atualização do backend.
Resumo
Saves persistentes em cloud streaming exigem tratar o filesystem da instância como efêmero e usar object storage durável como fonte da verdade. A arquitetura completa:
- IAM role concede às sessões de streaming acesso S3 de leitura/escrita escopado
- Script de launcher baixa os saves existentes antes do launch do jogo
- File watcher em background sincroniza mudanças para o S3 durante o gameplay com escritas com debounce
- Backend service constrói caminhos S3 por jogador a partir da identidade autenticada
- Verificações de integridade confirmam a correção de upload/download em cada sincronização
O custo em escala indie é de menos de $3/mês para 10.000 jogadores ativos. O tempo de implementação é de 1-2 dias se você construir você mesmo, ou 30 minutos se usar as APIs de dados do jogador do horizOn.
De qualquer forma, não lance um jogo em cloud streaming sem resolver esse problema. Seus jogadores não vão te dizer que os saves estão sumindo — eles simplesmente vão parar de jogar.
Fonte: Adding persistent game saves to Amazon GameLift Streams