Tutorials

    How to Fix ANRs in Production Android Apps

    A
    AzeemSep 27, 2026 · 10 min read
    85
    How to Fix ANRs in Production Android Apps

    Application Not Responding errors are frustrating because the app may not crash at all. Instead, the screen freezes, taps stop working, and users eventually force-close the app.

    Crashlytics may not show a traditional exception, but Android Vitals can still reveal the problem. In most cases, an ANR happens because the main thread is blocked by database work, network calls, JSON parsing, image processing, lock contention, or heavy startup logic.

    The goal is simple:

    Keep the main thread free for input, lifecycle events, and rendering.

    What Is an ANR?

    Android uses the main thread to:

    • Process touch and key events.

    • Run lifecycle callbacks.

    • Dispatch messages.

    • Update the UI.

    • Coordinate rendering.

    • Start activities and respond to system events.

    When the main thread is blocked for too long, Android considers the app unresponsive. An input-dispatch ANR commonly occurs when the app cannot respond to user input for approximately five seconds, although the exact timeout depends on the ANR type. Broadcast receivers, services, and content providers have different timeout rules.

    An ANR does not require an infinite loop. This innocent-looking code can still freeze the app:

    val orders = database.orderDao().getAllOrders()
    code

    It may work quickly on a flagship device with 500 rows but take several seconds on an older phone with a large database and slower storage.

    Why ANRs Usually Appear in Production

    ANRs are difficult to reproduce because production users have different:

    • Device performance.

    • Android versions.

    • Database sizes.

    • Network conditions.

    • Memory pressure.

    • Storage availability.

    • Battery settings.

    • App usage patterns.

    A flagship development phone can hide problems that affect Samsung A-series, Redmi, POCO, Oppo, Realme, Android Go, or other low-memory devices.

    The visible freeze may also be misleading. A frozen order screen could actually be caused by:

    • A database query during ViewModel creation.

    • A blocking binder call.

    • Large JSON parsing.

    • A lock held by another thread.

    • Heavy initialization in Application.onCreate().

    • Synchronous work inside a BroadcastReceiver.

    Do not guess from the screen that froze. Start with the production trace.

    Start With Android Vitals

    Open:

    Play Console → Android Vitals → Crashes and ANRs

    Google Play can show affected versions, devices, Android versions, ANR types, and stack traces when available.

    Check these details first:

    • Did the problem start after a specific release?

    • Which devices and Android versions are affected?

    • Is the issue concentrated in a particular manufacturer?

    • Is it an input, broadcast receiver, service, or startup ANR?

    • Is the user-perceived ANR rate increasing?

    • Which app version has the highest rate?

    If the issue appeared immediately after a release, inspect the code changed in that version first.

    The user-perceived ANR rate deserves special attention because it represents ANRs that users are more likely to experience directly, such as a frozen screen or unresponsive interaction.

    Read the Stack Trace

    Consider this simplified trace:

    "main" tid=1 Blocked
      at android.database.sqlite.SQLiteCursor.fillWindow(...)
      at com.app.orders.OrderRepository.getOrderHistory(OrderRepository.kt:67)
      at com.app.orders.OrderHistoryViewModel.<init>(OrderHistoryViewModel.kt:34)
    code

    This suggests:

    • The main thread is blocked.

    • SQLite is performing the operation.

    • The call came from OrderRepository.

    • The query was started while creating the ViewModel.

    The real problem is not simply “Room is slow.” The problem is that potentially slow database work was started synchronously on the main thread.

    Common trace clues

    Trace clue

    Possible cause

    SQLiteCursor, SQLiteConnection

    Database query

    FileInputStream.read

    File or disk access

    BitmapFactory.decode

    Image decoding

    org.json

    Large JSON parsing

    Collections.sort

    Expensive sorting

    BinderProxy.transact

    System or cross-process call

    Future.get, CountDownLatch.await

    Waiting for another thread

    Object.wait, ReentrantLock.lock

    Lock contention

    SharedPreferences...commit

    Synchronous preference write

    The first application-owned frame is often the most useful place to begin.

    Do not inspect only the main thread. Service, broadcast receiver, provider, binder, and lock-related ANRs may require examining other threads and sometimes system_server.

    Reproduce Real User Conditions

    When QA cannot reproduce the issue, make the test environment worse.

    Test with:

    • A mid-range or low-end device.

    • The oldest affected Android version.

    • A release or minified build.

    • A cold app start.

    • A large local database.

    • Slow or unstable network.

    • Battery Saver enabled.

    • Low storage and memory pressure.

    • Large images and long lists.

    • Existing user data instead of a fresh install.

    Use realistic data:

    500 rows: basic testing
    10,000 rows: useful performance testing
    100,000 rows: pagination and indexing testing
    Large images: decoding and memory testing
    code

    A query that takes 80 milliseconds on a flagship device may take several seconds on slower hardware.

    Enable StrictMode

    StrictMode can expose accidental disk and network work on the main thread during development.

    if (BuildConfig.DEBUG) {
        StrictMode.setThreadPolicy(
            StrictMode.ThreadPolicy.Builder()
                .detectDiskReads()
                .detectDiskWrites()
                .detectNetwork()
                .penaltyLog()
                .penaltyFlashScreen()
                .build()
        )
    }
    code

    You can also detect certain VM-level problems:

    if (BuildConfig.DEBUG) {
        StrictMode.setVmPolicy(
            StrictMode.VmPolicy.Builder()
                .detectLeakedClosableObjects()
                .detectActivityLeaks()
                .penaltyLog()
                .build()
        )
    }
    code

    StrictMode is an early warning system, not a complete ANR detector. It may not identify expensive sorting, large JSON parsing, or every lock-contention problem.

    Use it in debug or internal builds—not production.

    Confirm the Timeline With Perfetto

    A stack trace tells you where a thread was observed. Perfetto or System Trace helps explain what happened over time.

    Use:

    • Android Studio System Trace.

    • Perfetto.

    • CPU Profiler.

    • Macrobenchmark for repeatable startup and interaction tests.

    Look for:

    • Long blocks on the main thread.

    • SQLite activity.

    • Binder transactions.

    • Garbage collection.

    • RenderThread delays.

    • Input events waiting.

    • Lock contention.

    • system_server activity.

    For example:

    Main thread: SQLite query — 7.2 seconds
    RenderThread: waiting
    Input events: queued
    code

    This confirms that the UI was unresponsive because the main thread was occupied by a database operation.

    A running main thread may be doing expensive computation. A blocked or sleeping main thread may be waiting for disk I/O, a database lock, another thread, or a binder response. The fix depends on which situation you find

    Move Slow Work Off the Main Thread

    This pattern is risky:

    class OrderHistoryViewModel(
        private val repository: OrderRepository
    ) : ViewModel() {
    
        val orders = repository.getOrderHistory()
    }
    code

    If the repository performs disk I/O, this code may run on the main thread.

    Use a coroutine:

    class OrderHistoryViewModel(
        private val repository: OrderRepository
    ) : ViewModel() {
    
        private val _orders = MutableStateFlow<List<Order>>(emptyList())
        val orders = _orders.asStateFlow()
    
        init {
            viewModelScope.launch {
                _orders.value = repository.getOrderHistory()
            }
        }
    }
    code

    Keep the dispatcher decision inside the repository:

    class OrderRepository(
        private val orderDao: OrderDao
    ) {
        suspend fun getOrderHistory(): List<Order> =
            withContext(Dispatchers.IO) {
                orderDao.getAllOrders()
            }
    }
    code

    This protects every caller from accidentally running the query on the main thread.

    Use dispatchers according to the type of work:

    • Dispatchers.IO for blocking database, file, and network operations.

    • Dispatchers.Default for CPU-heavy sorting, compression, encryption, and transformations.

    • Main thread for lightweight state updates and UI rendering.

    Do not move everything to a background thread blindly. Also reduce the work itself.

    Fix the Query, Not Just the Thread

    Moving a query to Dispatchers.IO prevents the UI from freezing, but a slow query can still waste memory, battery, and time.

    Review whether the query:

    • Loads the entire table.

    • Reads unnecessary columns.

    • Lacks indexes.

    • Performs expensive joins.

    • Sorts a large result in memory.

    • Returns data the screen does not need.

    Prefer pagination and targeted queries:

    @Query(
        """
        SELECT id, customerName, total, createdAt
        FROM orders
        ORDER BY createdAt DESC
        LIMIT :limit OFFSET :offset
        """
    )
    suspend fun getOrders(
        limit: Int,
        offset: Int
    ): List<Order>
    code

    Use database-level filtering and sorting where possible. Add indexes based on real query patterns, then measure the result.

    The same principle applies to JSON parsing, image processing, and list transformations: move heavy work away from the main thread and reduce the amount of work required.

    Avoid Heavy Startup Work

    Startup ANRs are especially harmful because the user cannot even reach the first screen.

    Avoid doing this:

    override fun onCreate() {
        super.onCreate()
    
        analytics.init()
        database.open()
        remoteConfig.fetchBlocking()
        preloadEverything()
    }
    code

    Ask of every startup task:

    • Is it required before the first screen?

    • Can it run after the first frame?

    • Can it be initialized lazily?

    • Can it start only when the feature is opened?

    • What happens if it takes 10 seconds?

    Keep Application.onCreate() limited to essential initialization. Delay analytics, remote configuration, preloading, and noncritical SDK work whenever possible.

    Measure startup using Android Studio, Perfetto, and startup benchmarks instead of guessing. Android recommends tracing and benchmarking to identify startup bottlenecks.

    Watch Receivers, Services, and Locks

    Broadcast receivers

    Avoid long database, network, or computation work inside onReceive().

    For short background handoff, goAsync() may help:

    override fun onReceive(
        context: Context,
        intent: Intent
    ) {
        val pendingResult = goAsync()
    
        CoroutineScope(Dispatchers.IO).launch {
            try {
                syncData(context)
            } finally {
                pendingResult.finish()
            }
        }
    }
    code

    goAsync() is not designed for unlimited work. Use WorkManager or another appropriate scheduler for substantial or deferrable tasks.

    Locks

    Do not hold a lock while performing slow work:

    synchronized(cacheLock) {
        refreshCacheFromDisk()
    }
    code

    Instead, perform slow work first and update shared state briefly:

    val data = withContext(Dispatchers.IO) {
        readCacheFromDisk()
    }
    
    synchronized(cacheLock) {
        cache = data
    }
    code

    Also investigate these calls on the main thread:

    runBlocking { ... }
    future.get()
    latch.await()
    code

    They can wait indefinitely or for several seconds.

    Compose and Large Lists

    Do not perform blocking work directly inside a composable:

    @Composable
    fun OrderScreen(repository: OrderRepository) {
        val orders = repository.getOrderHistory() // Wrong
    }
    code

    Load data through a ViewModel:

    @Composable
    fun OrderScreen(viewModel: OrderHistoryViewModel) {
        val orders by viewModel.orders.collectAsStateWithLifecycle()
    
        OrderList(orders)
    }
    code

    For large lists:

    • Use lazy layouts.

    • Use stable keys.

    • Avoid sorting during recomposition.

    • Avoid rebuilding large collections unnecessarily.

    • Use pagination.

    • Filter and sort in the database when possible.

    • Decode images off the main thread with an image-loading library.

    A loading indicator cannot help if the main thread is blocked before it can draw the indicator.

    Common Fixes That Fail

    “Increase the timeout”

    This does not make the UI responsive. It may keep the blocking operation alive longer.

    “Use Dispatchers.IO everywhere”

    This may hide the main-thread problem while leaving a huge query, lock, memory issue, or startup dependency chain untouched.

    “Add a progress indicator”

    The indicator also needs the main thread to render it.

    “Test only on a flagship phone”

    This validates the best-case environment, not the production environment.

    “Rely only on Crashlytics”

    ANRs are not ordinary crashes. Use Android Vitals, traces, StrictMode, and Perfetto together.

    Release and Monitor the Fix

    Roll out the fix gradually when possible:

    5% → 25% → 50% → 100%
    code

    At each stage, monitor:

    • Overall ANR rate.

    • User-perceived ANR rate.

    • Affected devices.

    • Android versions.

    • App startup performance.

    • Reviews mentioning freezes.

    • New crashes or battery problems.

    Compare the fixed version with the last healthy version. If the original cluster disappears but a new cluster appears, the bottleneck may have moved rather than been eliminated.

    Final Checklist

    Before declaring the problem fixed, verify that:

    • The main thread performs no database or file I/O.

    • Network calls do not block UI callbacks.

    • Large JSON responses are parsed off the main thread.

    • Images are decoded and resized in the background.

    • Large lists use pagination or lazy loading.

    • Sorting and filtering are efficient.

    • runBlocking, Future.get(), and CountDownLatch.await() are not used on the main thread.

    • Locks are not held during slow operations.

    • Startup contains only essential work.

    • Receivers and service callbacks remain short.

    • StrictMode is enabled during development.

    • Perfetto has been used for unexplained cases.

    • Testing includes slow devices and realistic data.

    • The fix is monitored through a staged rollout.

    ANR debugging becomes much easier when treated as a production investigation rather than a random search for slow code:

    • Android Vitals shows where the issue occurs.

    • The trace shows what the app was waiting for.

    • Perfetto shows the timeline.

    • StrictMode catches many mistakes early.

    • Good repository boundaries keep blocking work away from the main thread.

    Keep the main thread available for input, lifecycle work, and rendering. Move slow work away from it—and reduce the amount of slow work your app needs to perform.

    anr-android
    kotin
    android app
    Was this helpful?

    Comments (0)

    No comments yet. Be the first to share your thoughts!

    Leave a Comment

    Get the latest updates

    Join our newsletter and never miss an article.

    By subscribing, you agree to our Privacy Policy and consent to receive updates.

    Ready to Start Learning?

    Join thousands of students building their dream careers

    Explore Courses