Crash reporting (Crashlytics, Sentry, Bugsnag) is a system design topic that touches mobile SDK design, server-side processing of millions of events, symbolication of native code, and the dashboards engineers actually use to debug. The interview tests whether you understand the layers and the engineering tradeoffs.
Functional requirements
- Capture crashes on the device — the SDK has to record the fault before the process dies, because a crashed app cannot retry or phone home after the fact.
- Capture non-fatal errors and exceptions — handled exceptions the app caught and recovered from, logged so you can watch error rates that don’t kill the app but still degrade the experience.
- Capture custom telemetry (breadcrumbs, custom events) — breadcrumbs are the trail of user actions and state changes leading up to a crash. Interviewers like to hear that you keep this in a small, fixed-size ring buffer so it can’t grow without bound.
- Send to backend — batch and compress reports, retry on failure, and back off when the device is offline so a flaky network never drops a report or drains the battery.
- Dedupe similar crashes; group by signature — one bug hitting a million users should be a single row in the dashboard, not a million. The choice of grouping key is where interviewers push hardest.
- Symbolicate (turn raw stack frames into source-level frames) — raw native stacks are hex addresses; turning them back into function names and line numbers is what makes a report actionable.
- Surface in a dashboard — engineers need to rank issues by user impact and drill into a single occurrence quickly.
SDK design
The mobile SDK installs handlers for:
- Uncaught exceptions (Java/Kotlin runtime exceptions on Android, NSException on iOS) — install a top-level handler (Thread.setDefaultUncaughtExceptionHandler on Android, NSSetUncaughtExceptionHandler on iOS) that runs after the runtime gives up but before the process exits.
- Native crashes (signal handlers — SIGSEGV, SIGABRT, etc.) — C/C++ and the OS deliver faults as POSIX signals; you register a handler that captures the register state and unwinds the native stack.
- ANRs (Application Not Responding) on Android — detected by checking whether the main thread services a posted message within roughly five seconds; a frozen UI thread is as bad as a crash to the user.
- Watchdog terminations on iOS (more difficult to capture) — the OS kills apps that block the main thread too long or use too much memory, and there is no signal to catch, so you infer them from the absence of a clean shutdown on the previous run.
SDK constraints:
- Tiny binary impact (target <500KB) — every added KB shows up in app download size, and teams will reject an SDK that bloats the binary, so keep third-party dependencies out.
- Zero startup time impact — installing handlers has to be cheap and off the critical path; heavy work at launch is an immediate reason for a team to drop the SDK.
- Robust to its own crashes (the crash reporter must not crash) — a bug in the reporter that takes down the host app is the worst possible outcome, so the crash path stays minimal, allocation-free where possible, and heavily tested.
Capture mechanics
When a crash occurs:
- SDK signal handler runs in a constrained context (no malloc, limited APIs)
- Captures crash metadata: stack frames, registers, threads, current breadcrumbs
- Writes to local disk (cannot send network from a crash handler)
- Process terminates
- On next app launch, SDK detects pending crash report and uploads
Symbolication
Native crash stacks are addresses, not function names:
0x100012345
0x100023456
Symbolication maps addresses to source-level frames using debug symbols (dSYMs on iOS, mapping files on Android).
Symbolication happens server-side after upload. Symbol files are uploaded by the build pipeline; the server cross-references on incoming crashes.
Dedup and grouping
Many crashes are the same bug from many users. Group by:
- Top 3–5 stack frames (after stripping non-app frames) — system-library frames are noise, so strip them and hash the top app frames. Use too many frames and one bug splits into many groups; too few and unrelated bugs merge together.
- Exception type — a NullPointerException and an OutOfMemoryError at the same frame are different bugs and belong in separate groups.
- App version — the same signature across releases is often tracked separately so you can tell whether a build fixed the bug or introduced it.
Each group is one issue in the dashboard. Users count is the impact metric.
Server architecture
Three pipelines:
- Ingest is a high-volume HTTP endpoint that queues events.
- Process symbolicates, groups, and persists events.
- Serve handles dashboard queries.
Volume scales fast: 1B events/day at the larger crash reporters. Architecture similar to Sentry, Datadog ingest pipelines.
Storage
- Hot store: recent events, dashboard-queryable (ClickHouse, Druid) — columnar stores handle the high-cardinality filtering and aggregation the dashboard runs over recent data.
- Cold store: archived events for forensic analysis (S3 + Parquet) — raw events are cheap to keep in object storage and are queried rarely, only when someone needs the full detail of an old occurrence.
- Metadata DB: issue metadata, user-issue mapping (Postgres) — issue state such as assignee and resolved flag, plus the user-to-issue mapping, is relational and low-volume, a good fit for a transactional database.
The dashboard
Engineers want:
- Sort issues by impact (users affected, occurrences) — affected-user count usually beats raw occurrence count, since one user in a crash loop shouldn’t outrank a bug hitting thousands of people once each.
- Filter by app version, OS, device — narrowing to something like “only iOS 17 on iPhone 15” is how engineers confirm a crash is device- or OS-specific.
- Drill into a single occurrence with breadcrumbs — the breadcrumb trail plus device and app state is what turns “it crashed” into a reproducible bug report.
- Mark issue as resolved; track regression detection — once resolved, the system watches for the same signature in later versions and reopens the issue if it comes back.
Privacy
- Strip PII from breadcrumbs and custom data automatically — scrub emails, tokens, and other identifiers on-device before upload using pattern matching and configurable deny-lists.
- Allow opt-out per user — give the host app an API to disable collection for users who decline, and honor it before anything is written to disk.
- Honor regional regulations (GDPR, CCPA) — support data-deletion and residency requirements, which in practice means being able to purge every event tied to a given user id.
- Never log auth tokens, passwords, or sensitive customer data — these should never reach your servers, so default to redaction and treat a leaked credential in a breadcrumb as a security incident.
Frequently Asked Questions
Why do some crashes never appear in the dashboard?
Crashes during app startup may not have a chance to write to disk. Watchdog kills (long main thread blocks) are not signal-handler-detectable on iOS. Both leave gaps.
How long does symbolication take?
Sub-second to a few seconds typically. If symbol files are large or missing, can take longer or fail.
How does crash reporting differ from APM?
Crash reporting captures fatal events. APM (DataDog, NewRelic) captures performance and traces. Increasingly the same tools cover both, but they began as separate disciplines.
Keep sharpening your system design:
