Question
Fixing "Inappropriate Blocking Method Call" in Kotlin Coroutines
Question
I am trying to use Kotlin coroutines more consistently, but I am running into a warning when I call libraries such as Moshi or OkHttp inside a coroutine:
// example shape of the problem
suspend fun loadData(): String {
val response = okHttpClient.newCall(request).execute() // warning here
return response.body!!.string()
}
The IDE shows the warning:
Inappropriate blocking method call
What does this warning mean in the context of coroutines, and what is the best way to handle blocking APIs like OkHttp or Moshi correctly?
Short Answer
By the end of this page, you will understand why Kotlin warns about blocking method calls inside coroutines, when that warning matters, and how to fix it properly using the right coroutine dispatcher such as Dispatchers.IO. You will also see how this applies to common libraries like OkHttp and Moshi, plus common mistakes to avoid.
Concept
In Kotlin coroutines, not all threads are meant for blocking work.
A blocking call is a function that keeps the current thread busy until it finishes. Examples include:
- Network requests using blocking APIs such as
OkHttpClient.newCall(...).execute() - Reading files from disk
- Database calls in blocking drivers
- Heavy parsing or serialization in some situations
Coroutines are lightweight, but the code inside them still runs on real threads. If you call a blocking method on a thread that should stay free, you can reduce performance or freeze important work.
That is why the IDE warns about "inappropriate blocking method call".
Why the warning appears
The warning usually appears when you call a known blocking API from a coroutine running on a dispatcher that is not intended for blocking operations, such as:
Dispatchers.Defaultfor CPU-bound workDispatchers.Mainfor UI work
Blocking on these dispatchers is a problem because:
Dispatchers.Maincan freeze the UIDispatchers.Defaulthas a limited pool meant for computation, so blocking it can starve CPU tasks
The usual fix
Move blocking work to Dispatchers.IO:
suspend : String = withContext(Dispatchers.IO) {
response = okHttpClient.newCall(request).execute()
response.body!!.string()
}
Mental Model
Think of coroutine dispatchers like different workstations in an office:
Dispatchers.Mainis the front desk. It must stay responsive.Dispatchers.Defaultis the analysis team. They do thinking and calculations.Dispatchers.IOis the back office for slow external tasks like waiting on files or network responses.
If you make the front desk employee wait on a package delivery for 10 minutes, nobody can greet visitors.
If you make the analysis team sit and wait on network calls, they stop doing useful computation.
So when a method blocks, send it to the back office: Dispatchers.IO.
Syntax and Examples
The core tool for moving work to the correct dispatcher is withContext.
Basic syntax
suspend fun example(): Result = withContext(Dispatchers.IO) {
// blocking code here
blockingCall()
}
Example with OkHttp
suspend fun fetchUserJson(client: OkHttpClient, request: Request): String {
return withContext(Dispatchers.IO) {
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
throw IOException("Unexpected HTTP ${response.code}")
}
response.body?.string() ?: throw IOException("Empty response body")
}
}
}
Why this works
execute()is a blocking network callwithContext(Dispatchers.IO)runs that block on a dispatcher meant for blocking I/Ouseensures the response is closed properly
Step by Step Execution
Consider this function:
suspend fun fetchTitle(client: OkHttpClient, request: Request): String {
return withContext(Dispatchers.IO) {
client.newCall(request).execute().use { response ->
response.body?.string() ?: ""
}
}
}
Here is what happens step by step:
fetchTitle()is called from a coroutine.withContext(Dispatchers.IO)suspends the current coroutine and schedules the block on an I/O thread.client.newCall(request).execute()starts a blocking HTTP request.- The I/O thread waits for the server response.
- Once the response arrives,
response.body?.string()reads the response body. usecloses the response automatically.- The result string is returned from the
withContextblock. - The coroutine resumes with that string.
Why this is safe
The coroutine is still written in a sequential style, but the blocking work happens on a dispatcher built for it.
What would be risky
suspend : String {
client.newCall(request).execute().body!!.string()
}
Real World Use Cases
Blocking calls inside coroutines appear often in real programs.
HTTP requests
withContext(Dispatchers.IO) {
client.newCall(request).execute()
}
Used in:
- REST API clients
- internal service calls
- downloading files
File reading and writing
val text = withContext(Dispatchers.IO) {
File("config.json").readText()
}
Used in:
- configuration loading
- log processing
- import/export tools
Database access with blocking drivers
val user = withContext(Dispatchers.IO) {
userRepository.findById(1)
}
Used in:
- server-side applications
- desktop apps
- Android apps using older or blocking persistence APIs
JSON parsing after network access
Moshi often appears right after an HTTP response is read. Developers frequently keep the fetch and parse steps together, especially when the payload is small.
Legacy library integration
Coroutines are often added to an existing codebase that already uses blocking Java libraries. withContext(Dispatchers.IO) is the standard bridge.
Real Codebase Usage
In real projects, developers usually do not scatter raw blocking calls everywhere. They wrap them in well-defined suspend functions.
Common pattern: repository wrapper
class UserRepository(
private val client: OkHttpClient,
private val adapter: com.squareup.moshi.JsonAdapter<User>
) {
suspend fun fetchUser(request: Request): User = withContext(Dispatchers.IO) {
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
throw IOException("HTTP ${response.code}")
}
val json = response.body?.string() ?: throw IOException("Empty body")
adapter.fromJson(json) ?: throw IOException("Invalid JSON")
}
}
}
This keeps blocking details in one place.
Guard clauses for validation
suspend fun fetchUser(request: Request?): User {
requireNotNull(request) { "Request must not be null" }
return withContext(Dispatchers.IO) {
TODO()
}
}
Common Mistakes
1. Calling blocking code directly from a suspend function
A suspend function is not automatically non-blocking.
Broken example:
suspend fun load(): String {
return client.newCall(request).execute().body!!.string()
}
Why it is a problem:
suspendonly means the function can suspend- it does not magically move work off the current thread
Fix:
suspend fun load(): String = withContext(Dispatchers.IO) {
client.newCall(request).execute().use { response ->
response.body?.string() ?: ""
}
}
2. Using Dispatchers.Default for blocking I/O
Broken example:
withContext(Dispatchers.Default) {
client.newCall(request).execute()
}
Why it is a problem:
Defaultis for CPU-bound tasks
Comparisons
| Situation | Best choice | Why |
|---|---|---|
Blocking network call with OkHttp execute() | Dispatchers.IO | It waits on I/O |
| File read/write | Dispatchers.IO | File access is blocking |
| Large JSON parsing from an in-memory string | Dispatchers.Default | It is mainly CPU work |
| UI update after loading data | Dispatchers.Main | UI work belongs on the main thread |
suspend vs withContext
| Concept | What it means |
|---|
Cheat Sheet
withContext(Dispatchers.IO) {
// blocking I/O work
}
Quick rules
suspenddoes not mean non-blocking by itself- Blocking network calls ->
Dispatchers.IO - Blocking file/database calls ->
Dispatchers.IO - CPU-heavy parsing/computation ->
Dispatchers.Default - UI updates ->
Dispatchers.Main
Safe OkHttp pattern
suspend fun fetch(): String = withContext(Dispatchers.IO) {
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("HTTP ${response.code}")
response.body?.string() ?: throw IOException("Empty body")
}
}
Common warning meaning
Inappropriate blocking method call usually means:
- you are calling a blocking API
- the current thread may not be appropriate for blocking work
Remember
FAQ
Why do I get "inappropriate blocking method call" in a coroutine?
Because the method you are calling blocks the thread, and the current dispatcher may not be intended for blocking work.
Is a suspend function automatically safe for blocking code?
No. A suspend function can still block the thread if you call a blocking API directly.
Should I always use Dispatchers.IO with OkHttp?
If you are using the blocking execute() method, yes, that is the normal choice.
Should Moshi run on Dispatchers.IO or Dispatchers.Default?
It depends. If the work includes blocking stream or response reading, use IO. If it is just heavy parsing of in-memory data, Default may fit better.
Can I ignore the warning?
Only if you are certain the code is already running on a suitable thread. In most cases, it is better to fix the dispatcher explicitly.
Is Dispatchers.Default faster than Dispatchers.IO?
Not for blocking I/O. Default is tuned for CPU-bound work, while IO is designed for blocking operations.
Mini Project
Description
Build a small suspend-based API helper that fetches JSON with OkHttp and parses it with Moshi without triggering blocking-call misuse. This demonstrates the correct way to wrap blocking library calls inside coroutines.
Goal
Create a suspend function that performs a blocking HTTP request safely on Dispatchers.IO, parses the result, and returns a Kotlin data object.
Requirements
- Define a simple data class to represent JSON data.
- Use OkHttp
execute()inside a suspend function. - Run the blocking network work on
Dispatchers.IO. - Parse the JSON response using Moshi.
- Throw a clear exception for HTTP errors or invalid data.
Keep learning
Related questions
Accessing Kotlin Extension Functions from Java
Learn how Kotlin extension functions are compiled and how to call them correctly from Java with clear examples and common pitfalls.
Android AlarmManager Example: Scheduling Tasks with AlarmManager
Learn how to use Android AlarmManager to schedule tasks, set alarms, and handle broadcasts with a simple beginner example.
Android Foreground Service Notification Channels in Kotlin
Learn why startForeground fails on Android 8.1 and how to create a valid notification channel for foreground services in Kotlin.