Home / Blog / Crash symbolication: the dSYM discipline that keeps stack traces readable

Crash symbolication: the dSYM discipline that keeps stack traces readable

You got a crash in Crashlytics, but the stack trace is hex addresses. Why? Your .dSYM files never made it. The discipline that keeps this from happening.

When a crash lands in Firebase Crashlytics (or Sentry, or Bugsnag) there are two possibilities:

Symbolicated: UserProfileView.updateName() at UserProfileView.swift:142. Readable, actionable.

Unsymbolicated: <redacted> or hex addresses. Completely useless.

The difference: are your .dSYM (debug symbol) files uploaded correctly to the crash reporter? Across 12 of my apps, this discipline cut debug time by 100x. This post walks through the correct .dSYM workflow.

What is a .dSYM?

When you build for release, Xcode produces two artifacts:

  1. App binary (.ipa): compiled machine code. Symbols stripped (debug info removed to keep the app small).
  2. dSYM bundle (.dSYM): the symbol database. Maps hex addresses to function names and line numbers.

When a crash reporter captures a crash, the stack trace comes back as hex addresses (symbols aren’t in the app). The dSYM resolves those addresses to symbol names.

Without the right dSYM, the crash report is unreadable. Firebase shows hex addresses and a warning: “we need dSYM for this build”.

Upload workflow: three variants

Three ways to get the .dSYM to the crash reporter:

1. Manual upload from Xcode after archiving:

  • Product > Archive
  • Organizer opens
  • “Download Debug Symbols” (for versions where Apple handled Bitcode)
  • Open the crash reporter’s dSYM upload dialog

Manual. Forget once and crash symbolication is gone.

2. Fastlane + run script:

# Fastfile
lane :deploy do
  gym(scheme: "MyApp")
  upload_to_testflight
  upload_symbols_to_crashlytics(
    dsym_path: lane_context[SharedValues::DSYM_OUTPUT_PATH],
    gsp_path: "./GoogleService-Info.plist"
  )
end

Automated via Fastlane. The upload_symbols_to_crashlytics action is ready to go for Crashlytics.

3. Build phase script (inside Xcode):

Add a new Run Script to the Build Phases section of the Xcode project:

# Crashlytics dSYM upload
"${PODS_ROOT}/FirebaseCrashlytics/upload-symbols" -gsp "${PROJECT_DIR}/GoogleService-Info.plist" -p ios "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}"

This script runs on every build automatically. You don’t want it for debug builds (pointless uploads). Gate it:

if [ "$CONFIGURATION" = "Release" ]; then
    # upload script
fi

Bitcode complication (pre iOS 16)

Bitcode let Apple re-compile your app binary on their side. The dSYM you uploaded could differ from the one produced after Apple’s re-compile.

Fix: download Apple’s new dSYM and upload it. The “Download Debug Symbols” button in Xcode Organizer.

Bitcode is deprecated in iOS 16. Disable it in new projects. This headache goes away when old projects upgrade.

Missing dSYM detection

The Crashlytics dashboard has a “Missing dSYM” section. Builds that produced crashes but never got a dSYM uploaded.

The list shows build versions: “v2.3.1 (build 47) – 18 crashes, no dSYM”.

This list is a warning signal. I check it weekly:

  • Build date under 7 days: something is wrong with my upload script
  • Old build: historical gap, fix it after the fact

Workaround for the historical gap: pull the dSYM from the old archive and upload manually.

App Store Connect API integration

App Store Connect generates new dSYMs after TestFlight and Store releases. To pipe these into Crashlytics automatically, use Fastlane’s download_dsyms plus upload_symbols_to_crashlytics:

lane :refresh_dsyms do
  download_dsyms(version: "latest")
  upload_symbols_to_crashlytics(gsp_path: "./GoogleService-Info.plist")
  clean_build_artifacts
end

Run this lane nightly on cron. It pulls the latest dSYM from App Store Connect, uploads to Crashlytics, and handles the different dSYM that Apple’s re-compile might produce.

Multi-build scenarios

When you have multiple build variants (staging, production, etc):

lane :deploy_staging do
  gym(configuration: "Staging")
  upload_symbols_to_crashlytics(
    dsym_path: lane_context[SharedValues::DSYM_OUTPUT_PATH],
    gsp_path: "./Staging/GoogleService-Info.plist"  # Staging Firebase project
  )
end

lane :deploy_production do
  gym(configuration: "Release")
  upload_symbols_to_crashlytics(
    dsym_path: lane_context[SharedValues::DSYM_OUTPUT_PATH],
    gsp_path: "./Production/GoogleService-Info.plist"  # Production Firebase project
  )
end

Separate GoogleService-Info.plist per Firebase project.

Alternative: Sentry

Sentry’s dSYM upload flow is a bit different:

lane :deploy do
  gym
  sentry_upload_dif(
    auth_token: ENV['SENTRY_AUTH_TOKEN'],
    org_slug: 'my-org',
    project_slug: 'my-app',
    path: lane_context[SharedValues::DSYM_OUTPUT_PATH]
  )
end

Sentry also ships sentry-cli: sentry-cli upload-dif ./path/to/dSYM. Call it from a script in your CI/CD pipeline.

Upload discipline

The workflow that actually works:

  1. Automated in the CI/CD pipeline. Every release build uploads automatically. No manual step.
  2. Verification. Check the Crashlytics / Sentry dashboard: “last upload: 2 hours ago”. Build freshness check.
  3. Weekly audit. Scan the missing dSYM list. Upload historically if needed.
  4. Alerting. If the CI/CD dSYM upload fails, the release still ships. Slack alert: “dSYM upload failed for v2.3.1”.

These four steps prevent symbolication issues.

dSYM storage: how long to keep them?

.dSYM files are big (100MB to 500MB per build). Crashlytics and Sentry keep their own copy. Do you also need to keep local copies?

Yes. Years later a crash report from an old build can surface. If the dSYM is gone, those hex addresses can’t be resolved.

Options:

  • Back up the Xcode Archives folder (~/Library/Developer/Xcode/Archives/) manually
  • Automatic upload to an S3 bucket: Fastlane’s upload_to_s3 action
  • Git LFS if you want to version dSYMs with your code repo (careful, they’re huge)

For Dentii I keep them in S3: s3://my-dsyms/dentii/v2.3.1-build47.dSYM.zip.

Performance impact

The dSYM upload’s effect on build time:

  • File size: 100 to 500MB per build
  • Upload time (internet dependent): 30 seconds to 5 minutes

1 to 2 extra minutes in the CI/CD pipeline. Worth running in parallel with the build where you can.

Troubleshooting: symbolication isn’t working

Crashes stay unsymbolicated, what do I do?

Check 1: does the Build UUID match?

The dSYM and the app binary must share a UUID. In Xcode: dwarfdump --uuid /path/to/app and dwarfdump --uuid /path/to/dSYM. Same UUID?

Different UUIDs means the wrong dSYM was uploaded.

Check 2: architecture match?

Separate dSYMs for arm64, arm64e, x86_64. App Store builds are arm64, simulator is x86_64. If you need both, upload both.

Check 3: did the upload succeed?

Check the “dSYMs uploaded” log on the Crashlytics/Sentry dashboard. Errors show there.

Check 4: wait time?

After a Crashlytics upload, processing can take 2 to 4 hours. It doesn’t resolve immediately.

Bottom line

.dSYM upload discipline is the topic iOS developers skip the most. Crashes come in, can’t be debugged, time is lost.

Fastlane + CI/CD automation handles the discipline. A weekly audit catches missing dSYMs. Local backup keeps you covered years later.

A one to two hour setup investment makes every crash report actionable. An iOS app without this workflow isn’t production ready.

Have a project on this topic?

Leave a brief summary — I’ll get back to you within 24 hours.

Get in touch