runBlocking — what it is, blocking bridge and how it works

Author: IT Sectr Published: 2026-06-22 Reading time: 7 min

runBlocking — a Coroutine Builder in Kotlin that blocks the current thread until the passed coroutine completes. Unlike launch and async, it is not a suspend function and can be called from regular (blocking) code. According to JetBrains documentation, 2024, runBlocking serves as a bridge between the synchronous and asynchronous worlds, allowing you to launch coroutines from main functions and tests.

Key Takeaways

  • runBlocking — a blocking builder that creates a CoroutineScope and waits for the coroutine to complete
  • Thread blocking — runBlocking holds the current thread until the coroutine and all its children fully complete
  • Entry points — main(), JUnit tests and bridging between blocking and async code
  • Forbidden on Main thread Android — calling runBlocking on the UI thread causes ANR
  • Alternatives — lifecycleScope, viewModelScope, TestCoroutineDispatcher for Android

What is runBlocking?

runBlocking is a Kotlin function that creates a new CoroutineScope and launches the passed coroutine, blocking the current thread until it fully completes. Unlike all other Coroutine Builders, runBlocking is not a suspend function and can be called from regular synchronous code. The runBlocking signature takes a CoroutineContext and a suspend block, returning a result of type T.

kotlin
public fun <T> runBlocking(
    context: CoroutineContext = EmptyCoroutineContext,
    block: suspend CoroutineScope.() -> T
): T

runBlocking launches a new event-loop on the current thread. When the coroutine calls a suspend function (e.g., delay() or await()), runBlocking blocks the thread and executes other scheduled coroutines on the same thread until the suspended one resumes. This is cooperative blocking — the thread is not idle but processes other coroutines.

How runBlocking works

The internal mechanism of runBlocking is based on an event-loop: when a suspend function is called, runBlocking pauses execution of the current block and runs other coroutines from the queue. When the suspend function completes, execution resumes. This cycle continues until all coroutines are finished.

Event-loop under the hood

runBlocking uses its own single-threaded pool for executing coroutines. Unlike Dispatchers.IO or Default, runBlocking does not switch threads — it handles all coroutines on the current thread, interleaving their execution. This is the only builder that guarantees execution on the same thread.

kotlin
fun main() {
    val threadName = Thread.currentThread().getName()
    println("Before runBlocking on $threadName")

    val result = runBlocking {
        println("Inside runBlocking on ${Thread.currentThread().getName()}")
        delay(500L)
        "Done"
    }

    println("After runBlocking: $result")
}

The output will show that all three println statements execute on the same thread. runBlocking does not switch threads but organizes cooperative multitasking within a single thread using an event-loop.

When to use runBlocking

runBlocking is justified in three scenarios: the main() entry point in console applications, unit tests for suspend functions, and bridging — calling suspend code from callback-based or blocking libraries. In production Android code, using it on the main thread is strictly forbidden.

ScenarioApplicabilityRisks
main() of console applicationYesNone — it is the entry point, thread does not block UI
JUnit testsYesMinimal — tests are synchronous by definition
Android UI threadNoANR, lags, interface freezing
Callback → CoroutineYes, with cautionThread pool blocking with long operations

For Android tests, use kotlinx-coroutines-test with TestDispatcher instead of runBlocking. This gives time control, automatic cleanup, and test isolation.

Alternatives to runBlocking

In most scenarios, runBlocking can and should be replaced with async alternatives. For Android, these are viewModelScope, lifecycleScope, or CoroutineScope with the proper dispatcher. For tests — TestCoroutineDispatcher and runTest.

kotlin
    // Bad: runBlocking on Android main thread
runBlocking(Dispatchers.Main) {
    val result = networkApi.fetchData()
    textView.setText(result)
}

// Good: lifecycleScope
lifecycleScope.launch {
    val result = withContext(Dispatchers.IO) { networkApi.fetchData() }
    textView.setText(result)
}

For tests, the replacement is runTest from kotlinx-coroutines-test. It creates a TestCoroutineScope with virtual time, allowing you to test delays without real waiting. This speeds up tests and makes them deterministic.

runBlocking usage examples

The most common scenario is testing suspend functions. runBlocking in tests allows you to synchronously wait for a coroutine result without changing architecture. The second scenario is libraries with callback APIs, where suspend functions are called from a blocking context via runBlocking.

kotlin
// Test suspend function with runBlocking
class RepositoryTest {
    @Test
    fun `fetchUser returns correct data`() {
        val repository = UserRepository(FakeApi())

        val result = runBlocking {
            repository.fetchUser("123")
        }

        assertEquals("John", result.name)
        assertEquals("john@test.com", result.email)
    }
}

For bridging between callback and suspend worlds, use CompletableDeferred in combination with runBlocking instead of callbacks — this simplifies async operation chains and improves code readability.

Dangers of misuse

Improper use of runBlocking is one of the common mistakes when transitioning from a blocking approach to coroutines. Main problems: calling on the Android Main thread, nesting runBlocking inside each other, using it inside async functions, and launching long-running operations via runBlocking.

  • ANR — runBlocking on the Android Main thread blocks UI rendering for more than 5 seconds
  • Deadlock — nested runBlocking inside a coroutine on the same thread leads to mutual deadlock
  • Dispatcher confusion — Dispatchers.Main inside runBlocking on a background thread has no Looper and throws an exception
  • Memory leaks — runBlocking is not automatically cancelled when Activity/Fragment is destroyed

Golden rule: runBlocking is a bridge, not a replacement. Use it only to connect blocking and non-blocking worlds. For all other tasks, use launch, async, or lifecycleScope.

Frequently Asked Questions

Why does runBlocking block the thread while other builders do not?

runBlocking is the only builder that is not a suspend function. It starts an event-loop on the current thread and does not return control until all coroutines are completed. launch and async return control immediately, executing the coroutine in the background.

Can runBlocking be used in Android ViewModel?

Not recommended. ViewModel has a built-in viewModelScope which automatically manages coroutines and cancels them when destroyed. runBlocking in ViewModel blocks the thread and does not respond to lifecycle cancellation.

What to use instead of runBlocking in unit tests?

Use runTest from the kotlinx-coroutines-test library. It provides a TestCoroutineScope with virtual time control, automatic cancellation, and deterministic execution.

What is an event-loop in runBlocking?

Event-loop is an event processing cycle inside runBlocking. When a coroutine suspends (e.g., delay()), the event-loop switches to executing other ready coroutines on the same thread. This creates the illusion of multitasking without thread switching.

What happens when runBlocking is called inside runBlocking?

Nested runBlocking on the same thread creates a deadlock — the outer block waits for the inner one, but the inner cannot start until the outer finishes. On different threads it is allowed but strongly discouraged due to debugging complexity.

Summary

  • runBlocking — a blocking Coroutine Builder, bridge between blocking and async code
  • Event-loop runBlocking processes coroutines cooperatively on a single thread without switching
  • Allowed scenarios — main(), JUnit tests, bridging from callback libraries
  • Forbidden scenarios — Android UI thread, nested calls, long-running operations
  • Alternatives — lifecycleScope, viewModelScope, runTest for tests
  • ANR risk — runBlocking on the Main thread causes app freeze after 5 seconds
  • For production Android code, use async builders — runBlocking is not designed for UI

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also