Guide
Diagnosing and Fixing Main‑Thread Blocking from Kotlin runBlocking in Android Apps
Learn how to spot, diagnose, and resolve UI freezes caused by Kotlin's runBlocking on Android's main thread, with step‑by‑step checks, code fixes, and verification tips.
Published by Tasadduq Burney
04 Aug 2025, 22:42 UTC
3 min88.1K views0

Recognizable Condition
The application’s UI becomes unresponsive or triggers an ANR (Application Not Responding) when a user interacts with a screen that launches a coroutine using runBlocking on the main (UI) thread. The freeze lasts for the duration of the blocking operation, and the main thread stops processing input events and drawing frames.
Cause / Diagnostic Table
| Observable Symptom | Underlying Cause | What to Look For |
|---|---|---|
| UI stops responding for >16 ms (jank) or ANR dialog appears | runBlocking blocks the thread it is called on until the coroutine completes | Main thread stack trace shows runBlocking at the top, with no dispatcher specified (implicitly uses the caller’s thread) |
| Long‑running spikes in CPU/Thread‑State profiles | The blocking work (e.g., file I/O, network) runs on the main thread | Profiler flame graph reveals a continuous block under runBlocking → yourBlockingCall() |
Ordered Checks
- Reproduce the freeze: Perform the user action that triggers the coroutine (e.g., button tap) and note the UI latency.
- Capture the main‑thread stack:
- Open Android Studio Profiler → CPU → Record while the freeze occurs.
- Alternatively, run
adb logcatand filter formainthread traces when the ANR appears.
- Identify
runBlocking: In the recorded trace, verify that the topmost frame on the main thread is a call tokotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsKt.runBlocking(or the Kotlin‑std‑lib equivalent). No dispatcher argument should be present. - Confirm the blocked work: Look at the frames below
runBlockingto see the actual suspending function (e.g., network request, file read) that is executing on the main thread.
Fixes Tied to Findings
When the offending code is in a UI component (Activity, Fragment, View)
// BEFORE – problematic
fun loadData() {
runBlocking { // blocks the main thread
val result = repository.fetch() // suspending network call
display(result)
}
}
// AFTER – non‑blocking
fun loadData() {
viewModelScope.launch { // tied to UI lifecycle, uses Dispatchers.Main by default
withContext(Dispatchers.IO) { // move blocking work off the main thread
val result = repository.fetch()
}
display(result) // back on Main via viewModelScope
}
}
When the code resides in a plain Kotlin file (e.g., utility)
// BEFORE
suspend fun processData() = runBlocking {
// heavy CPU work
heavyComputation()
}
// AFTER – expose a suspending function and let the caller decide the dispatcher
suspend fun processData() = withContext(Dispatchers.Default) {
heavyComputation()
}
// Caller example in UI:
// viewModelScope.launch { val data = processData() }
Escalation Criteria
- If UI remains unresponsive after replacing
runBlocking: Verify that no other blocking calls (e.g.,Thread.sleep, synchronous I/O) are still executed on the main thread. Use the same profiling steps to locate any remaining long‑running frames. - If thread‑pool exhaustion appears: Ensure that background dispatchers (
Dispatchers.IO,Dispatchers.Default) are not being over‑subscribed. Consider limiting concurrent work with a customCoroutineDispatcheror usingSemaphore‑based throttling. - If the blocking work must stay on the main thread (rare): Refactor the work to be truly asynchronous (e.g., use Android’s
Handler,Looper, or platform APIs) rather than forcing a coroutine to block.
Verification
- Run the app on a device or emulator.
- Perform the action that previously caused the freeze.
- Open Android Studio Profiler → CPU → Record.
- Confirm that the main thread shows no spikes exceeding ~16 ms during the operation.
- Optionally, write a unit test using
kotlinx.coroutines.test.TestScopeandStandardTestDispatcherto assert that the suspending function completes without blocking when invoked on the test dispatcher.
Limitations
runBlockingis still appropriate in unit tests,mainfunction entry points, or when deliberately blocking a non‑UI thread (e.g., a background service).- The fix assumes the suspending function itself is safe to move to a background dispatcher; if it relies on UI‑only resources, you must marshal results back to the main thread (as shown with
viewModelScope.launch). - Heavy CPU‑bound work should use
Dispatchers.Default; I/O‑bound work should useDispatchers.IO. Mis‑matching dispatchers can still cause jank or thread‑pool starvation.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.