All articles
August 30, 20263 min read#flutter#stability#crashlytics#production

From 80% to 98% Crash-Free: A Stability Playbook for Flutter Apps in Production

The step-by-step process we used at Mengantar to raise the crash-free user rate from 80% to 98% on an app with over 100,000 concurrent users. No rewrites, just discipline and the right instrumentation.

When I joined the Mengantar project the Flutter app had a crash-free user rate of about 80%. Two in ten people opening the app would hit a crash in a session. Eighteen months later it was holding at 98% with far more users. Here is the exact order we did things in, because the order mattered as much as the fixes.

Step 1: Make every crash visible

You cannot fix what you cannot see. The first week went entirely to instrumentation:

  • Firebase Crashlytics wired into FlutterError.onError and PlatformDispatcher.instance.onError so both framework and async errors are captured.
  • Native crash collection enabled on Android and iOS, because a surprising share of "Flutter crashes" are native SDK crashes underneath.
  • Custom keys on every report: screen name, user role, app flavour, and the last three navigation events.
  • dSYMs and ProGuard mapping files uploaded automatically from CI. Unsymbolicated stack traces are decoration.

Step 2: Sort by users affected, not by count

Crashlytics shows you issues sorted by event count by default. Switch to users affected. One crash that fires 5,000 times for 12 users is a loop bug; one that fires 600 times for 600 users is your real problem. We fixed the top five by users affected before touching anything else. That alone moved the rate from 80% to about 90%.

Step 3: Kill the null and cast crashes at the boundary

The majority of remaining crashes were data shape problems: a field the backend started sending as null, a number arriving as a string, an enum value nobody had seen. The fix was not a thousand null checks in widgets. It was one rule: parsing happens in the data layer, with DTOs that tolerate missing fields and map to entities with sane defaults. The UI never sees raw JSON. Once that boundary existed, a whole category of crashes stopped appearing.

Step 4: Treat ANRs and frozen frames as crashes

Users do not distinguish between a crash and a frozen screen. We added performance traces for the three heaviest flows and found a synchronous JSON decode of a large order list on the main isolate. Moving it to compute and paginating the request removed the ANRs and, as a side effect, most of the "app is slow" reviews.

Step 5: Gate releases on the number

This is the step most teams skip. We added a rule to the release checklist: a build cannot go past 20% rollout unless the crash-free rate for that version is at or above the previous version. Rollouts were paused three times in the first six months. Each pause caught a regression that would have reached everyone.

Step 6: Keep a weekly 30-minute stability review

Every week, the top three new issues by users affected get an owner and a decision: fix now, fix next sprint, or accept with a reason written down. Thirty minutes. It is the most valuable recurring meeting on the project because it stops small regressions from becoming the next 80%.

What did not help

  • Wrapping everything in try/catch. It hides problems and makes the reports useless.
  • Bumping every dependency "to be safe". It introduced two new crashes for every one it fixed.
  • A rewrite. The temptation was real. The data said the architecture was fine and the inputs were not.

Stability is a process you run, not a milestone you reach. The rate held at 98% because the process kept running after the big fixes were done.

Written by
Ghozi Mahdi
Senior Mobile Engineer · Founder, Rancangin Aja
Get in touch