Performance & Crash Debugging

Debug Android ANRs: Start with the Blocked Main Thread

An Android ANR indicates that a component failed to respond within a system-defined expectation. Start by determining what the main thread was waiting for and which thread or operation owned the blocking work.

By , FounderUpdated 4 min read

Identify the ANR category

The ANR guide describes different causes and timing contexts. Do not apply one universal timeout number to every input, service or broadcast situation.

Capture the reported reason, process, affected version and thread traces. A stack showing an idle main thread can be a late snapshot rather than proof that no blockage occurred.

Follow the dependency chain

Main-thread state Investigation
Doing CPU work Expensive computation or serialization
Waiting on a lock Which thread owns the lock?
Blocking I/O Disk, network or provider operation
Waiting on another task Deadlock or synchronous bridge

Inspect other threads as well as the main stack. Moving work to a background thread does not help if the UI immediately blocks waiting for its result.

Correlate surrounding events

Use Logcat and available system traces to align the ANR with user actions, lifecycle events, SDK initialization and resource pressure.

Avoid discarding timestamps or process information when redacting logs. Remove credentials and personal data while preserving the timing needed for diagnosis.

Fix ownership and scheduling

Make blocking work asynchronous with lifecycle-aware completion. Reduce lock scope, avoid calling unknown external code while holding a lock, and keep broadcast/service work within the appropriate execution model.

Do not hide the symptom by adding a longer application timeout when the system's responsiveness requirement still applies.

Verify under stress

Test slow storage/network, repeated navigation and representative lower-end devices. APKLint's Crash / ANR Analyzer can help interpret redacted traces, but it cannot reconstruct every scheduling event from one stack snapshot. Confirm the fix with a reproducible scenario and watch the affected release cohort after rollout.

Work through a blocked-thread example

Imagine an ANR trace in which the main thread is waiting to enter SessionStore.save, while a worker holds the same monitor and performs a slow disk operation. The method named at the top of the main-thread stack is where the UI stopped, but the worker owns the work that must finish before it can proceed. Copy the lock identity, find the owning thread and read that thread's stack before changing the UI method.

A useful repair hypothesis is to build a small immutable snapshot while holding the lock, release the lock, then persist that snapshot asynchronously. This is a design example, not a universal patch: establish how concurrent updates are ordered so an older snapshot cannot overwrite a newer one. Replacing synchronized with a different lock type without changing the ownership or duration would preserve the same blocking dependency.

Capture a bounded reproduction

Use a synthetic account and a local development build. Record the app version, device model, Android version and exact sequence of actions. Reproduce the screen transition or request that precedes the freeze, then keep the thread dump and surrounding timestamped logs. Android's ANR documentation explains that ANR causes include blocking operations and lock contention; the relevant system reason matters when interpreting a trace.

For a controlled slow-storage case, inject a delayed persistence implementation into a test build instead of filling a user's device or running unbounded stress. Confirm whether navigation remains responsive while the operation is pending. For a network-related case, use your test endpoint's delayed response and distinguish a background request from an accidental main-thread wait for its future. A timeout in the HTTP client does not help the UI if the thread still blocks for the entire timeout interval.

Avoid a misleading stack snapshot

A main thread captured in its message loop can be idle by the time evidence is collected. Do not close the incident solely because that one frame looks normal. Align the system's ANR timestamp with the application's logs and any available trace, and compare reports from the same action and release. Conversely, a busy frame observed during ordinary profiling does not by itself establish an ANR; it needs the timing and responsiveness context.

Annotate what is observed separately from what is inferred. “Main waits on monitor X; worker Y owns X during persistence” is evidence. “Storage was slow because the user's phone is old” is a hypothesis unless measurements establish it. This distinction prevents the next fix from optimizing an unrelated computation that happens to appear in the same stack.

Verify cancellation and repeated actions

After making the asynchronous change, repeat the original action, navigate away while the work runs, and trigger the action again. Check that completion does not update a destroyed screen and that duplicate actions cannot create conflicting writes. Measure input responsiveness in the reproduction and compare traces under the same artificial delay. Then test without the artificial delay so the diagnostic hook itself is not mistaken for production behavior.

Retain the original and repaired traces with a short explanation of the removed dependency. APKLint can help organize stack information, but a parsed trace does not certify scheduling behavior on every device. A controlled reproduction plus observed responsiveness provides a stronger basis for deciding that this specific ANR path is repaired.

Sources and further reading

  1. Android Developers: ANRs
  2. Android Developers: View logs with Logcat

Reference review: 29 September 2026. Examples illustrate the workflow; check your installed versions, release artifact and account-specific Console requirements before applying them. This guide is not a claim that APKLint executed your project or verified your private account.

APKLint

Android inspection tools and practical release guides. About APKLint · Report a correction

Treat every result here as one checked input into a release decision, not the whole decision. Analyzer findings describe what is verifiable from the artifact itself: the manifest, the package structure, the certificates and the bytecode surfaces. Confirm anything load-bearing against the actual build you ship, because a source tree and a packaged APK can tell different stories.

When a finding surprises you, reproduce it before fixing: change one thing in the project, rebuild, and scan again. The delta between runs is the fastest way to prove a fix landed — and a result that does not move when the input changes means the input was not what you thought it was.

Coverage notes matter as much as findings: what was inspected, what was skipped, and what was flagged in the inspected part. A clean result over a partially-scanned file is weaker evidence than a flagged finding over a fully-scanned one, so read the coverage line first. For anything touching policy, certificates or third-party SDKs, close the loop against the Android developer documentation and the Play policy center — tools speed the review up, they do not replace it.

Reproducibility is the bar for anything in this guide: if you cannot reproduce a finding on the current artifact, do not act on it yet. Change one variable, rebuild, scan again, and only trust the delta you can explain.