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 fun loadData(): String = withContext(Dispatchers.IO) {
val response = okHttpClient.newCall(request).execute()
response.body!!.string()
}
Dispatchers.IO is designed for blocking I/O tasks like:
- network access
- disk access
- database calls
What about Moshi?
Moshi itself is often used for JSON parsing. Parsing is usually CPU work, not I/O. But if your parsing is tied to reading from a blocking stream or response body, then the whole operation may belong in Dispatchers.IO.
A simple rule:
- Blocking network/disk/stream access ->
Dispatchers.IO - Pure CPU-heavy processing ->
Dispatchers.Default
Important idea
The warning does not mean you cannot use blocking libraries in coroutines. It means you should run them on the right dispatcher.
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
Example with OkHttp + Moshi
data class User(val id: Int, val name: String)
suspend fun fetchUser(
client: OkHttpClient,
request: Request,
adapter: com.squareup.moshi.JsonAdapter<User>
): User {
return withContext(Dispatchers.IO) {
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
throw IOException("Unexpected HTTP ${response.code}")
}
val json = response.body?.string() ?: throw IOException("Empty response body")
adapter.fromJson(json) ?: throw IOException("Invalid JSON")
}
}
}
This is reasonable because the blocking network read happens inside Dispatchers.IO.
If parsing is large and CPU-heavy
Sometimes you may separate I/O from parsing:
suspend fun fetchAndParse(
client: OkHttpClient,
request: Request,
adapter: com.squareup.moshi.JsonAdapter<User>
): User {
val json = withContext(Dispatchers.IO) {
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("HTTP ${response.code}")
response.body?.string() ?: throw IOException("Empty body")
}
}
return withContext(Dispatchers.Default) {
adapter.fromJson(json) ?: throw IOException("Invalid JSON")
}
}
This split is useful when parsing is expensive enough to matter.
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 fun fetchTitleBad(client: OkHttpClient, request: Request): String {
return client.newCall(request).execute().body!!.string()
}
This code may run on whatever dispatcher the caller used. If that is Main or Default, the warning is justified.
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) {
// blocking call
TODO()
}
}
Validate early before doing expensive work.
Error handling
suspend fun safeFetch(request: Request): Result<User> {
return try {
val user = withContext(Dispatchers.IO) {
// blocking call and parsing
TODO()
}
Result.success(user)
} catch (e: Exception) {
Result.failure(e)
}
}
Separation of concerns
Teams often separate layers like this:
- API layer performs network I/O on
Dispatchers.IO - parsing may stay there for small payloads or move to
Dispatchers.Defaultif heavy - UI or controller layer simply calls suspend functions
Dependency-injected dispatchers
In larger codebases, dispatchers are often injected for testability:
class UserService(
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
suspend fun load(): String = withContext(ioDispatcher) {
"data"
}
}
This makes unit tests easier.
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- blocking it can reduce throughput
Use Dispatchers.IO instead.
3. Doing blocking work on Dispatchers.Main
Broken example:
withContext(Dispatchers.Main) {
client.newCall(request).execute()
}
This can freeze the UI.
4. Forgetting to close the response
Broken example:
val response = client.newCall(request).execute()
return response.body!!.string()
Better:
client.newCall(request).execute().use { response ->
return response.body?.string() ?: ""
}
5. Assuming all Moshi work belongs on Dispatchers.IO
If Moshi is parsing an already loaded string and the JSON is large, that is more like CPU work.
Rule of thumb:
- reading from network/file stream:
Dispatchers.IO - expensive parsing/computation:
Dispatchers.Default
6. Silencing the warning without fixing the cause
Do not just ignore the warning unless you know the current thread is already correct. The goal is to make the code actually safe, not only quiet.
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 |
|---|---|
suspend fun | A function that can suspend without blocking the caller thread |
withContext(...) | Run a block in a specific coroutine context or dispatcher |
A suspend function does not automatically choose the correct dispatcher.
OkHttp execute() vs async coroutine-friendly style
| Approach | Type | Notes |
|---|---|---|
execute() | Blocking | Use inside Dispatchers.IO |
callback-based enqueue() | Non-blocking callback API | Can be wrapped into coroutines if needed |
If a library already offers a coroutine-native API, that is often nicer than wrapping a blocking call yourself. But for blocking APIs, withContext(Dispatchers.IO) is the standard approach.
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
Dispatchers.IOis the usual fix for blocking libraries- keep blocking code wrapped in dedicated suspend functions
- use
use {}forResponsecleanup - do not block
Main - do not waste
Defaulton I/O
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.
What is the simplest correct fix?
Wrap the blocking call in withContext(Dispatchers.IO) { ... }.
Do I need to use callbacks instead of coroutines for OkHttp?
No. You can keep coroutine-style code and call blocking APIs safely on Dispatchers.IO.
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.
Add a Maven Repository URL in Gradle Kotlin DSL (build.gradle.kts)
Learn how to add Maven Central and a custom Maven repository URL in Gradle Kotlin DSL using build.gradle.kts syntax.
Allow HTTP and HTTPS in Android 9 Pie with Network Security Configuration
Learn how Android 9 Pie handles cleartext HTTP traffic and how to allow HTTP and HTTPS safely using network security config.