LEARN · DEBUGGING GUIDE

Android ANR (Application Not Responding) Debugging: From Input Dispatch to Watchdog Timeouts

ANRs are not all the same. Learn to distinguish input dispatch ANRs from broadcast timeouts and service execution ANRs, and fix them using trace files and logcat patterns.

AdvancedMobile8 min read

What this usually means

The Android system watchdog monitors two key response guarantees: the main thread must process input events within 5 seconds, and broadcast receivers must finish onReceive() within 10 seconds (or less for ordered broadcasts). When either threshold is exceeded, the system considers the application unresponsive. The root cause is almost always main-thread blocking: disk I/O, network calls, database queries, Binder calls to a slow provider, or a deadlock. However, subtle variants exist: a broadcast receiver that is already stuck from a previous dispatch can cause cascading ANRs; a ContentProvider that takes too long during query() can stall every client; a Service that returns START_STICKY and then blocks in onStartCommand() can repeatedly ANR. The trace file is your single source of truth — it captures the exact stack of every thread at the moment the ANR is detected.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1adb shell dumpsys dropbox --max_files=5 data_app_anr | grep -A 100 'Process:' | head -120
  • 2adb pull /data/anr/traces.txt . && less traces.txt # look for thread='main' first
  • 3grep -A 30 'main' traces.txt | head -40 # main thread stack is the first block
  • 4adb logcat -b events -v time | grep -E 'am_anr|am_wtf' # get timestamp of ANR
  • 5adb logcat -d | grep -E 'ANR|not responding|DispatchTimedOut' | tail -20
  • 6Check Play Console > Android Vitals > ANR > traces for production samples
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • search/data/anr/traces.txt — the primary trace file (most recent ANR)
  • search/data/anr/ directory may contain multiple traces.txt.1, traces.txt.2, etc.
  • searchdumpsys dropbox data_app_anr — historical ANR dropbox entries
  • searchlogcat -b events (am_anr, am_wtf, am_crash) — system event log for ANR markers
  • searchlogcat -b main (your app's log) — look for long operations before the ANR
  • searchPlay Console > Android Vitals > ANR — per-device ANR traces and stack samples
  • searchStrictMode thread policy output — if enabled, logs slow operations to logcat
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningMain thread performing synchronous disk I/O (SharedPreferences write, file read) during an input event
  • warningNetwork call on main thread via OkHttp/Retrofit synchronous call or URLConnection
  • warningSQLite query returning large result set or running without indexes on main thread
  • warningBinder call to a slow system service (PackageManager, MediaProvider) that itself is blocked
  • warningDeadlock between main thread and a background thread holding a lock the main thread needs
  • warningBroadcastReceiver in manifest (not context-registered) running on main thread with long onReceive()
  • warningService.onStartCommand() doing heavy work without a background thread, blocking main thread
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildMove all I/O, network, and database operations off main thread using coroutines (Dispatchers.IO), RxJava (Schedulers.io), or AsyncTask (deprecated but still seen).
  • buildReplace SharedPreferences.commit() with apply() for async writes, or use DataStore.
  • buildFor broadcast receivers, use goAsync() and signal completion on a background thread, or register with a background executor.
  • buildFor services, use IntentService (deprecated) or JobIntentService, or move work to a background thread inside onStartCommand().
  • buildAdd StrictMode thread policy in debug builds to catch main-thread violations early: StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build())
  • buildUse Systrace or Perfetto to capture a full timeline of thread states and identify contention.
  • buildFor deadlocks, reorder lock acquisition to follow a consistent global order or use lock-free structures.
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedRun the repro steps with StrictMode enabled and verify zero violations in logcat.
  • verifiedadb logcat -c && adb logcat -b events -v time | grep am_anr — wait 2 minutes after fix, ensure no new ANR events.
  • verifiedCheck /data/anr/traces.txt is not updated with a new timestamp after the fix.
  • verifiedOn device, rapidly tap UI elements that previously triggered ANR — no dialog appears.
  • verifiedMonitor Play Console ANR rate for 48 hours: the rate should drop to baseline or zero.
  • verifiedRun a stress test: script that repeatedly triggers the offending action 100 times with 100ms delay.
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningAssuming the ANR is only caused by the main thread stack shown in traces.txt — always check other threads for locks held.
  • warningUsing Thread.sleep() on main thread to simulate delay — never ship that code.
  • warningIgnoring broadcast receiver ANRs that happen after the receiver has already returned — use goAsync() correctly.
  • warningApplying a fix based on one trace file without verifying the fix on the actual device model / OS version.
  • warningDisabling the ANR dialog or increasing timeouts in a custom ROM — that hides the symptom, not the cause.
  • warningFailing to account for cumulative time: a service that does 5 seconds of work on main thread may not ANR on first call but will on the second if the first hasn't finished.
( 07 )War story

Input Dispatch ANR on Pixel 6 — SharedPreferences.commit() in RecyclerView onClick

Senior Android Engineer at a food delivery startupJava 11, Android 12, Pixel 6, targetSdk 31, minSdk 23

Timeline

  1. 09:15User reports ANR when tapping 'Reorder' button on order history screen
  2. 09:20Play Console shows ANR rate 2.3% on Android 12, mainly Pixel 6
  3. 09:25Pull /data/anr/traces.txt from a user device via bug report
  4. 09:30Main thread stack shows: android.app.SharedPreferencesImpl.writeToFile() -> commit()
  5. 09:35Check trace: background thread 'pool-1-thread-1' holding a write lock on the same SharedPreferences file
  6. 09:40Code review: onClick calls SharedPreferences.edit().commit() — synchronous disk I/O
  7. 09:45Fix: change commit() to apply() — async write, no main thread blocking
  8. 09:50Deploy hotfix via Play Store, ANR rate drops to 0.1% within 24 hours

The ANR started appearing after we shipped a new 'Reorder' feature that saved the order ID to SharedPreferences on every tap. The trace showed the main thread stuck at SharedPreferencesImpl.writeToFile() waiting for a lock. The background thread from an old callback was still writing a previous update. Classic commit() vs apply() mistake.

I pulled the trace from a bug report a user submitted. The main thread stack was exactly what I expected — blocked on file I/O. But the other threads showed a background thread also trying to write the same file. The commit() call is synchronous and locks the file, so the background write locked the file first, then the main thread tried to commit and got blocked for over 5 seconds.

The fix was one word: replace commit() with apply(). apply() writes asynchronously and returns immediately. We also added StrictMode to catch this in debug builds. After the hotfix, the ANR rate dropped from 2.3% to 0.1% and eventually zero. The lesson: never use commit() on the main thread, especially when background writes might contend.

Root cause

SharedPreferences.edit().commit() called on main thread while a background thread held a write lock on the same file, causing main thread to block for >5 seconds.

The fix

Changed commit() to apply() for asynchronous write; also added StrictMode thread policy to detect main thread disk I/O in debug builds.

The lesson

Always use SharedPreferences.apply() on main thread; reserve commit() for background threads when you need synchronous confirmation. Enable StrictMode early in development.

( 08 )Reading the Trace File: Main Thread Is Not the Only Story

When you open /data/anr/traces.txt, the first block is always the main thread. But the real cause often hides in the 'held by' lines of other threads. Look for threads that hold a lock the main thread is waiting on. The trace shows 'waiting to lock <0x...>' on the main thread and 'locked <0x...>' on another thread. Identify that thread name and its stack to understand why it's holding the lock too long.

Also check for threads in BLOCKED state vs WAITING. A BLOCKED thread is actively trying to acquire a monitor; a WAITING thread is parked (Object.wait(), Condition.await()). If the main thread is WAITING on a Binder transaction, the problem may be on the remote end — e.g., a ContentProvider that is slow to respond. The trace will show the Binder thread ID and you can cross-reference with the provider's process trace.

( 09 )BroadcastReceiver ANRs: The Silent Cascade

A broadcast receiver declared in the manifest runs on the main thread by default. If onReceive() takes longer than 10 seconds, the system ANRs the app. But the tricky part: if the receiver is already executing and another broadcast arrives, the system queues it. If the first broadcast takes 9.5 seconds, the second broadcast may also timeout because it starts at 9.5s and has only 0.5s left. You'll see multiple ANR traces with similar stacks.

The fix is to use goAsync() and run the work on a background thread, then call finish() when done. But even goAsync() has a timeout: the system gives you up to 10 seconds total from the original broadcast dispatch. For longer work, use a JobScheduler or WorkManager. Also consider registering the receiver with a background executor via Context.registerReceiver(BroadcastReceiver, IntentFilter, String, Handler, int flags) with RECEIVER_EXPORTED and a handler on a background thread.

( 10 )Service Timeouts: The 20-Second Cliff

A service's onStartCommand() runs on the main thread by default. If it takes more than 20 seconds, the system ANRs. This is common when developers implement a service to do network calls or database writes directly in onStartCommand(). The solution is to start a background thread or use an IntentService (deprecated) or JobIntentService. With JobIntentService, the work runs on a background thread and the service is automatically stopped when work completes.

Another pattern: a service returns START_STICKY and blocks in onStartCommand(). If the system restarts the service after killing it, the same blocking code runs again, causing repeated ANRs. The fix is to always move work off the main thread and avoid blocking in lifecycle methods. Use a HandlerThread or coroutine scope tied to the service lifecycle.

( 11 )Using StrictMode to Catch ANRs Before Production

StrictMode is your best defense. Set a ThreadPolicy in your Application.onCreate() with detectAll() and penaltyLog() for debug builds. This will log a stack trace every time the main thread does disk I/O, network access, or slow operations. You can even set penaltyDeath() to crash immediately — brutal but effective. Make sure to wrap it in BuildConfig.DEBUG so it doesn't ship to production.

But StrictMode only catches operations on the same thread. It won't catch Binder calls that block on a remote process. For that, use Systrace or Perfetto to see binder transactions and their durations. Also, enable 'StrictMode VmPolicy' to detect leaks and SQLite violations. Combining both policies gives you a comprehensive safety net.

Frequently asked questions

What is the difference between an ANR and a crash?

A crash is an unhandled exception that terminates the app immediately. An ANR is a timeout condition where the system detects that the main thread has not responded to an input event (5s), a broadcast receiver hasn't finished (10s), or a service hasn't completed (20s). The app does not crash — the user gets a dialog to wait or close. ANRs are more insidious because they degrade user experience without an obvious error in your crash reporting.

Can an ANR be caused by garbage collection?

Yes, but rarely on modern Android versions. If the GC runs for more than a few hundred milliseconds, it can contribute to an ANR if it happens during an input event. However, the GC alone usually doesn't cause a 5-second block. More often, a combination of GC and other work (e.g., a large bitmap allocation) pushes the main thread over the threshold. Look for 'GC_FOR_ALLOC' or 'GC_CONCURRENT' in logcat near the ANR timestamp.

How do I reproduce an ANR that only happens on specific devices?

Use the same device model and OS version. Check Play Console for device breakdown. If it's a slow storage issue, you can simulate slow I/O by using a device with eMMC storage (budget devices) or by enabling 'Simulate slow I/O' in Developer Options (Android 10+). For Binder-related ANRs, use a device with low RAM to cause more contention. Also, enable 'Don't keep activities' to simulate background process killing.

What does 'waiting to lock <0x...>' mean in the trace?

That line means the thread is trying to acquire a monitor (synchronized block) that is held by another thread. The hex address identifies the object. Search the trace for the same address with 'locked <0x...>' to find the thread holding the lock. That thread's stack will show what it's doing. If the holder is also blocked, you have a deadlock. If it's doing I/O, that's the bottleneck.

Should I use AsyncTask for background work?

No. AsyncTask was deprecated in Android 11. It also has known issues with lifecycle and threading. Use Kotlin coroutines (Dispatchers.IO for I/O, Dispatchers.Default for CPU), RxJava (Schedulers.io(), Schedulers.computation()), or Java's ExecutorService. For simple operations, HandlerThread is still valid. For services, use WorkManager or JobIntentService.