SDK & Framework Guides

Firebase Crashlytics NDK Setup: Symbolicate Native Crashes

Step-by-step Firebase Crashlytics NDK native library symbolication setup guide. Enable NDK crash reporting, automate symbol uploads, and read symbolicated traces.

Native Development Kit (NDK) crashes in Android apps are among the hardest to diagnose. When your Kotlin or Java code calls into C/C++ libraries, a segfault or memory corruption happens deep in unmanaged code—and without proper symbol uploads, you'll see only memory addresses instead of function names. Firebase Crashlytics can capture these native crashes, but the firebase crashlytics NDK native library symbolication setup requires a few deliberate steps to transform raw stack traces into readable, actionable data.

This guide walks through enabling NDK crash reporting, configuring automatic symbol uploads, and reading symbolicated traces so you can actually fix native crashes instead of guessing.

Why NDK Crashes Are Silent by Default

When your app crashes in native code, Firebase Crashlytics does see the event—but without symbols, the stack trace is useless:

#00 pc 0x0001a234 /data/app/com.myapp/lib/libmylib.so
#01 pc 0x00023456 /data/app/com.myapp/lib/libnative.so
#02 pc 0x00015abc libc.so

These are memory offsets, not function names. The only way to map them back to source code is to upload symbol files (.so.symbols files or breakpad symbols) that correspond to each release. Without symbols, even your best detective work won't crack the case.

NDK crashes span multiple scenarios: third-party native libraries you don't control, your own C/C++ code, or platform-level code invoked through JNI. Symbolication works for all of them—if symbols are available.

Prerequisites and Gradle Setup

Before enabling NDK crashes, ensure:

  • Android Gradle Plugin 7.1+ (or equivalent; older versions have weaker symbol integration).
  • NDK installed on your machine and in your Android Studio SDK manager.
  • Gradle build cache and .gradle/cxx directory available (Crashlytics uses these to locate symbol files).
  • Firebase Crashlytics Gradle plugin version 2.9.0 or later.

Update your app's build.gradle.kts (or build.gradle):

plugins {
  id("com.android.application") version "8.0.0"
  id("com.google.gms.google-services")
  id("com.google.firebase.crashlytics")
}

dependencies {
  implementation("com.google.firebase:firebase-crashlytics:18.6.0")
  implementation("com.google.firebase:firebase-crashlytics-ndk:18.6.0")
}

The -ndk artifact is critical; it adds JNI hooks to capture native signals and crashes.

Enabling NDK Crash Capture

Once the NDK dependency is added, Firebase Crashlytics automatically hooks into native crash handlers at runtime—no extra initialization code needed. However, you must ensure crashes are reported:

// In your Activity or Application class
import com.google.firebase.crashlytics.FirebaseCrashlytics

val crashlytics = FirebaseCrashlytics.getInstance()
// Crashes are captured automatically; no explicit call required
// But you can test or add context:
crashlytics.setCustomKey("user_segment", "premium")

If you want to manually test a native crash (for verification), you can use a minimal JNI function, but in production, crashes happen on their own—Firebase intercepts them via signal handlers.

Enable Crashlytics debug logging during development. Set FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(true) early in your Application class, and use adb logcat | grep Crashlytics to watch initialization.

Configuring Automatic Symbol Uploads

Symbol uploads are the linchpin. Firebase Crashlytics provides a Gradle task that automatically uploads symbols to Firebase Console during your release build.

Step 1: Enable Symbol Uploads in Build Configuration

In your app's build.gradle.kts:

android {
  buildTypes {
    release {
      firebaseCrashlytics {
        nativeSymbolUploadEnabled = true
        strippedNativeLibrariesDir = "build/intermediates/stripped_native_libs/release"
      }
    }
  }
}

The strippedNativeLibrariesDir tells Gradle where to find .so files. If you use a custom NDK build system (CMake, ndk-build), ensure the output lands in a predictable location.

Step 2: Verify Google Services Credentials

Gradle needs to authenticate with Firebase to upload symbols. The Gradle plugin uses credentials from your google-services.json file (already in your project root). Ensure the Firebase project in google-services.json matches your Firebase Console project.

Step 3: Run the Release Build

Build and upload in one step:

./gradlew :app:assembleRelease

At the end of the build, you'll see:

Task :app:uploadCrashlyticSymbols
Uploading 3 symbol files to Firebase Crashlytics...
Upload completed successfully.

If the task is skipped or fails, check:

  1. Is nativeSymbolUploadEnabled = true? Recheck your build type.
  2. Are .so files present in the configured directory? Gradle only uploads files it finds.
  3. Is your Firebase authentication valid? Run ./gradlew tasks | grep crashlytics to see available tasks.

Handling Multiple ABIs and Release Variants

If your app targets multiple Android ABIs (armeabi-v7a, arm64-v8a, x86, x86_64), Gradle will build separate .so files for each. Firebase uploads all of them automatically—one symbol file per ABI per release version.

For multiple build variants (e.g., release, staging), ensure each has the symbol-upload task enabled:

android {
  buildTypes {
    release {
      firebaseCrashlytics { nativeSymbolUploadEnabled = true }
    }
    staging {
      firebaseCrashlytics { nativeSymbolUploadEnabled = true }
    }
  }
}

Upload happens for each variant independently, so you can deploy staging builds to testers and still track native crashes.

Reading Symbolicated Crash Traces

Once symbols are uploaded and a native crash occurs in production, open Firebase Console → Crashlytics, select the crash group. The stack trace now shows real function names:

#00 pc 0x0001a234 libmylib.so (my_native_function+4)
#01 pc 0x00023456 libnative.so (JNI_OnLoad+456)
#02 pc 0x00015abc libc.so (abort+12)

Each frame is now clickable. Firebase can link to source if you have Source Maps enabled (though that's more useful for web; native crashes primarily benefit from function names and line-number metadata).

If a crash still shows addresses instead of names, symbols for that .so file weren't uploaded. Common causes:

  • Symbols were uploaded for version 1.0.0, but this crash came from version 1.0.1 and symbols weren't re-uploaded.
  • A third-party library ships without symbols; nothing you can do there.
  • The ABI doesn't match (you uploaded arm64-v8a symbols, but the crash was on armeabi-v7a).

If you strip symbols from production binaries (common for app-size optimization), the .so files you upload must be the pre-strip versions. Gradle's uploadCrashlyticSymbols task uses unstripped .so files from the build intermediates, so this usually works automatically—but if you manually strip, keep the originals for upload.

Integration with Error Tracking Platforms

Firebase Crashlytics is Firebase-only. If you're building multi-platform error monitoring—for instance, correlating native crashes on Android with Python backend errors or Go service crashes—you'll want a platform that speaks all those languages. Most how to debug production errors guides cover both mobile and backend; what is error tracking explains the broader story.

For mobile-first teams, Firebase is often sufficient. But if you're running add error tracking to your app across a polyglot stack—especially if you need distributed tracing to connect frontend crashes to backend failures—a hosted alternative like LightTrace can ingest native crashes via the same Sentry SDK protocol, in parallel with or instead of Firebase, and gives you a unified dashboard across platforms.

Verifying Your Setup

To confirm NDK crashes are being captured and symbolicated:

  1. Deploy a release build to a test device or internal testers.
  2. Trigger a native crash (call abort() or a null-pointer dereference from JNI code; for real apps, this happens naturally).
  3. Wait 10–60 seconds for Firebase to receive the crash.
  4. Open Firebase Console → Crashlytics → check the latest issue group.
  5. Confirm function names appear in the stack trace.

If symbols appear, you're done. If only addresses show, symbols weren't uploaded for that release version—re-run the build and upload step.


NDK crash symbolication is straightforward once the plumbing is in place. The Gradle plugin handles symbol collection and upload automatically; you just need to enable it, authenticate, and deploy. Symbolicated native crashes turn cryptic memory addresses into readable, debuggable stack traces—the difference between "it crashes somewhere in a library" and "it crashes in my_native_function at line 42, dereferencing a null pointer."

Start tracking errors in minutes

Start tracking native crashes today—try LightTrace free, and if you're also monitoring frontend or backend errors, unify them in one dashboard with distributed tracing and Sentry SDK compatibility.

Fix your next production error faster

Point any Sentry SDK at LightTrace — free up to 5,000 events/month.