Opening
Have you ever seen that dreaded dialog? The one that says "App is not responding — wait or close?" That is an ANR — Application Not Responding. It is one of the worst things that can happen to your app in production. Users hate it. The Play Store tracks it. It kills your app's ratings and rankings. But here is the thing — an ANR is not random. It is not a mystery. It is a precise, measurable failure with a specific cause. The system sent a message to your main thread, and your main thread did not process it in time. That is it. And once you understand the mechanics — the timers, the thresholds, the message queue we talked about last video — you can predict exactly when an ANR will happen and prevent it every time.
How ANRs Actually Work
Let us be very precise about the mechanism. The system sends a message to your app's main thread — an input event like a tap, or a broadcast intent, or a service start command. At the same time, it starts a timer. For input events, the timer is 5 seconds. For broadcast receivers, it is 10 seconds. For foreground services, it is a shorter window defined by the system. If your main thread processes the message and returns before the timer expires, everything is fine. The timer is cancelled. But if the timer fires before your main thread has finished processing the message, the system declares an ANR. It collects diagnostic information — including stack traces from every thread in your process — writes them to a file, and then shows the user that "not responding" dialog. The critical insight is that the system does not watch your CPU usage or your memory. It watches whether your main thread processes specific messages within specific time windows. That is the only criterion.
The 5-Second Rule for Input
The most common ANR trigger is the input dispatch timeout. When the user touches the screen, the system sends that touch event to your app. If 5 seconds pass and your app has not finished handling that touch, you get an ANR. But it is actually more nuanced than that. The system does not start the timer when your app receives the touch event. It starts the timer when the system tries to deliver the next input event and finds that the previous one has not been acknowledged. So if the user taps and your app is stuck processing something, the ANR will not fire at exactly 5 seconds after the tap. It fires when the system tries to deliver the next event — like a second tap, or even a key event — and realizes the previous one is still pending. This means ANRs from input events require a second input event to trigger the detection. One tap into a frozen app is not enough — the user needs to interact again for the system to notice.
What Causes ANRs
So what causes your main thread to be stuck for 5 seconds or more? Let me walk through the usual suspects. Number one: network calls on the main thread. You make an HTTP request, the server is slow, and your main thread blocks for 10 seconds waiting for the response. This is so common that Android has StrictMode to detect it. Number two: heavy database operations on the main thread. A Room query that scans a large table. An unindexed query on SQLite. Even "fast" queries can be slow if the disk is under pressure from other processes. Number three: lock contention. Your main thread tries to acquire a synchronized lock, but a background thread is holding it while doing heavy work. The main thread blocks waiting for the lock. This one is insidious because the actual heavy work is on a background thread — it just happens to hold a lock the main thread needs. Number four: too many messages in the queue. Even if each individual message is fast, if there are thousands of messages backed up, the cumulative processing time might push past the threshold. Number five: SharedPreferences commit on the main thread. The apply method is asynchronous, but commit blocks.
Broadcast Receiver ANRs
Broadcast receivers have their own ANR rules. When the system delivers a broadcast to your app, it expects you to handle it quickly. If your BroadcastReceiver's onReceive method does not return within 10 seconds, the system declares an ANR. Now, 10 seconds sounds generous, but remember — onReceive runs on the main thread. If your main thread is already busy with other work, the broadcast sits in the queue. By the time the Looper gets to it, several seconds might have already elapsed. Then your onReceive handler does some work, and by the time it returns, you have crossed the 10-second threshold. The fix is to keep onReceive extremely lightweight. If you need to do real work in response to a broadcast, start a coroutine, schedule a WorkManager job, or start a foreground service. Do not do the work in onReceive.
ANR Traces: The Diagnostic Gold Mine
When an ANR occurs, the system writes diagnostic traces to a file on the device at data/anr/traces.txt. This file is an absolute gold mine. It contains the stack trace of every thread in your process at the moment the ANR was detected. The most important thread to look at is main — thread number one. The stack trace tells you exactly what the main thread was doing when it got stuck. Was it in a database call? You will see SQLite methods in the stack. Was it waiting on a network response? You will see socket read methods. Was it blocked on a lock? You will see "waiting to lock" with the monitor address and the thread that holds it. You can also look at other threads to see what they are doing — maybe a background thread holds a lock that the main thread is waiting for. Reading ANR traces is a skill that separates senior Android developers from everyone else. The stack trace does not lie — it tells you exactly what happened.
How to Read an ANR Trace
Let me walk you through reading one. You open the trace file and look for the main thread section. It will say something like "main" prio=5 tid=1 and then a state. The state is crucial. If it says "Blocked," the main thread is waiting to acquire a monitor lock. Look for "waiting to lock" followed by a hex address and "held by thread" with a thread ID. Go find that thread and see what it is doing — that is the root cause. If the state says "Waiting," the main thread might be in Object.wait, waiting for a condition that never arrives. If the state says "Runnable" or "Native," the main thread is actively doing work. Look at the stack trace to see what work — is it in your code doing computation? Is it in a framework method doing IO? Is it in a native library? The stack reads bottom to top — the bottom is where execution started, the top is where it currently sits. Start from the top and look for the first frame that is your code or a library you recognize. That is where to focus.
StrictMode: Catching Problems Early
StrictMode is your early warning system. It is a developer tool that detects common main-thread violations during development so you can fix them before they become ANRs in production. You enable it in your Application.onCreate with StrictMode.setThreadPolicy. You can configure it to detect disk reads, disk writes, network operations, and slow calls on the main thread. When a violation is detected, you can choose to log it, flash the screen, show a dialog, or crash the app. I recommend crashing during development — it forces you to fix the issue immediately rather than ignoring a log message. StrictMode also has a VM policy that detects leaked closeable objects, untagged sockets, and other system resource issues. Enable both policies during development. Disable them — or at least change the penalty to logging — in production builds. StrictMode is essentially a canary. It catches the small violations before they become five-second freezes.
Coroutines Do Not Save You
Here is a misconception I need to address directly. Some developers think that because they use coroutines, they are safe from ANRs. That is wrong. Coroutines help you move work off the main thread, but they do not magically make blocking code non-blocking. If you write withContext Dispatchers.Main and then call a synchronized method that blocks for 6 seconds, you have an ANR. The coroutine is suspended in the sense of the coroutine framework, but the underlying thread — the main thread — is blocked. Dispatchers.Main means "run this on the main thread." If the code you run on the main thread blocks, the main thread is blocked. Period. Similarly, if you launch a coroutine on Dispatchers.IO but then use a Mutex that the main thread is also trying to acquire, you can create a deadlock that causes an ANR. Coroutines change how you express concurrency. They do not change the fundamental rules of threading. The main thread is still a single-lane highway, and if you park a car on it, nothing else gets through.
Monitoring ANRs in Production
In development, you catch ANRs visually — your app freezes, you notice. But in production, you need monitoring. The Play Console shows your app's ANR rate and flags you if it exceeds thresholds — more than 0.47 percent of sessions with an ANR is considered bad behavior. Google Play Vitals will show you the ANR clusters, including the main thread stack traces grouped by similarity. Crash reporting tools like Firebase Crashlytics also capture ANR reports. But the most proactive approach is to monitor your main thread's responsiveness in real time. You can post a message to the main thread and measure how long it takes to execute. If it regularly takes more than 100 milliseconds, your queue is backed up. Some performance monitoring SDKs do exactly this — they post a canary message on a timer and alert when the main thread's response time exceeds a threshold. This lets you detect and fix queue congestion before it becomes a full ANR.
Preventing ANRs
Let me give you a concrete prevention checklist. One: never do network IO on the main thread. Always use a coroutine with Dispatchers.IO or an async HTTP library. Two: never do disk IO on the main thread. Use DataStore instead of SharedPreferences, and make sure Room queries run on a background dispatcher. Three: minimize lock contention. If you must use synchronized blocks, keep them as small as possible and never hold a lock while doing slow work. Consider using concurrent data structures instead of synchronizing. Four: keep BroadcastReceiver.onReceive fast. Delegate heavy work to a service or coroutine. Five: use StrictMode during development to catch violations early. Six: profile regularly. Use Android Studio's CPU profiler to see what the main thread is doing during critical user journeys. Seven: watch your Play Vitals dashboard. If your ANR rate is climbing, investigate immediately.
Recap and What is Next
Let us recap. An ANR happens when the system sends a message to your main thread and your main thread does not process it within the timeout — 5 seconds for input events, 10 seconds for broadcasts. Common causes are network or disk IO on the main thread, lock contention, and excessive queue congestion. The system writes ANR traces to data/anr/traces.txt, showing every thread's stack trace at the moment of failure. StrictMode catches violations during development. Coroutines help you structure code off the main thread, but they do not prevent blocking if you use them incorrectly. In the final video of this phase, we are going to talk about something even more surprising than ANRs — process death. Your app is killed far more often than you think, and most Android bugs come from assuming it is not. See you there.