Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/AmolPardeshi99/android-performance-skills/llms.txt

Use this file to discover all available pages before exploring further.

BroadcastReceiver.onReceive runs on the main thread and must complete within 10 seconds (foreground) or 60 seconds (background) before the system fires an ANR. The commonly misunderstood goAsync() API does not pause this timer — it still runs from the moment onReceive starts.

Heavy Work in onReceive

Any blocking call inside onReceive — network I/O, database access, file reads — holds the main thread and pushes toward the ANR threshold. The system has no patience here: input events queued during a blocked onReceive begin contributing to the 5-second input ANR clock at the same time.

Bad: network call directly in onReceive

// ❌ BAD: network call in onReceive — blocks Main for unknown duration
class PushReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val result = networkClient.syncNow()  // BLOCKS Main
        showNotification(context, result)
    }
}

The goAsync() Misconception

goAsync() does not stop the ANR timer. It converts the synchronous lifecycle callback into an asynchronous one, but the 10-second timeout runs continuously from the moment onReceive starts until PendingResult.finish() is called. If your background work exceeds 10 seconds, the ANR fires even if your main thread returned immediately from onReceive.
This is one of the most common mistakes in Android broadcast handling. Developers see that goAsync() “hands off” execution and assume the ANR clock resets or pauses. It does not. The system is measuring wall-clock time from the start of onReceive, full stop.

Subtle bug: goAsync used but finish() arrives after timeout

// ❌ SUBTLE BUG: goAsync is used but PendingResult.finish() may be called after 10s timeout
// ANR still fires if the sub-thread work exceeds the broadcast timeout limit
class PushReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val pending = goAsync()               // timer is still running from this point
        Thread {
            Thread.sleep(12_000)              // 12s of work — ANR fires at 10s even in background thread
            pending.finish()
        }.start()
    }
}

Correct Option A: goAsync with a Coroutine and try/finally

Use goAsync() only for work that is reliably short — well under 10 seconds. Always call pending.finish() in a finally block so it is invoked even when an exception is thrown.
// ✅ CORRECT option A: use goAsync() only for short work (<10s); always call finish()
class PushReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val pending = goAsync()
        CoroutineScope(Dispatchers.IO).launch {
            try {
                val result = networkClient.syncNow()
                showNotification(context, result)
            } finally {
                pending.finish()  // REQUIRED: call this before timeout, or system ANRs anyway
            }
        }
    }
}

Correct Option B: Delegate to WorkManager

Prefer WorkManager for anything that might take longer than a few seconds. WorkManager runs your work in a separate process context with proper lifecycle management, retry logic, and constraints — and it removes all ANR risk from the broadcast receiver itself.
When the broadcast signals that background work needs to happen — syncing data, downloading a file, processing a notification payload — enqueue a OneTimeWorkRequest and return immediately. The receiver exits in microseconds.
// ✅ CORRECT option B: delegate to WorkManager (preferred for anything >few seconds)
class PushReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        WorkManager.getInstance(context).enqueue(
            OneTimeWorkRequestBuilder<SyncWorker>().build()
        )
        // onReceive returns immediately; no pending.finish() needed
    }
}

Decision Guide

Use goAsync() when:
  • The work is guaranteed to complete in well under 10 seconds (e.g., a quick cache update or a local database write)
  • You need the result before returning from the broadcast lifecycle
  • You always call pending.finish() in a finally block
Use WorkManager when:
  • The work involves network I/O, large file operations, or anything with unpredictable duration
  • You need retry on failure, constraint handling (requires network, charging), or deduplication
  • The broadcast is from a background context where the 60-second limit still leaves too much uncertainty
When in doubt, use WorkManager.

Build docs developers (and LLMs) love