Quick answer: Emit debug symbols from every release CI build, upload to a symbol server keyed by build ID, and fail the build if upload fails. Crash reporters symbolicate on demand by fetching the matching symbols. Do not rely on humans to upload.
A player crashes. The report lands in your tracker with a stack trace full of hex addresses. You try to symbolicate and the tool says “build ID not found.” Somebody forgot to upload symbols for that release. Now you have a crash you cannot read. The fix is one CI step that runs every time.
Per-Platform Symbol Files
Each platform has its own debug format:
- Windows:
.pdbalongside the.exe. - macOS / iOS:
.dSYMbundle alongside the binary. - Linux: DWARF debug info embedded or split into
.debugfiles. - Android: native symbols for
.solibraries plus ProGuard/R8 mapping files. - Consoles: platform-specific formats via each SDK.
Every shipped binary has a unique build ID embedded. Symbols are matched to binaries by this ID.
The CI Pipeline Step
# GitHub Actions example for Windows
- name: Build release
run: build-release.bat
- name: Upload symbols to S3
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET }}
run: |
for pdb in build/*.pdb; do
build_id=$(dumpbin /HEADERS "$pdb" | grep -oP '(?<=ID )[A-F0-9]+')
aws s3 cp "$pdb" "s3://game-symbols/windows/$build_id/$(basename $pdb)"
done
- name: Verify upload
run: verify-symbol-upload.sh ${{ github.sha }}
The verify step checks that every expected symbol file is on the server. If any are missing, the CI job fails and the release is blocked.
Retention Policy
Keep symbols for every shipped build forever. A crash from a year-old build is still useful to investigate if you can symbolicate it. Storage is a few GB per release at most; a few dollars per month on S3 is nothing compared to an unsymbolicatable crash.
For development/internal builds, keep symbols for 30 days. That covers the iteration window and prevents unbounded growth.
Symbolication On Demand
Your crash reporter symbolicates when reports arrive. Most open-source (Breakpad, Crashpad, Sentry) and commercial services handle this automatically if you point them at your symbol server. For custom pipelines, fetch symbols by build ID and run llvm-symbolizer or addr2line.
The Most Common Failure
Somebody builds a release locally, uploads the binary without uploading symbols, and ships. All crashes from that release are unsymbolicatable. Prevent this by making the release build require a CI artifact. No artifact, no release. Local builds cannot be shipped.
“Symbols you didn’t upload are symbols you don’t have. Automate the upload, fail the build on failure, and never lose a stack trace again.”
Related Issues
For cross-platform crash report handling, see how to handle platform-specific crash reports. For build automation overall, see how to build automated smoke tests for game builds.
Write a CI check that fails if even one symbol file is missing from the upload. Silent partial uploads are worse than loud total failures.