Navigating Kotlin's "suspend" and "runBlocking": A Cautionary Tale.

Similar to threads in Java, Kotlin uses coroutines to run the tasks in the background.

In Kotlin, you can use coroutines in few predefined methods like :

  1. runBlocking

  2. launch

  3. suspend

  4. coroutineScope

  5. async

Based on each keyword the usage and implementation of coroutines is different.

In this article, we are going to discuss, why we should be careful in using the suspend keyword.

Generally speaking, in mobile apps development, runBlocking shall not be used as it will block the current thread until the task at hand is completed, which is a problem as there would be a single thread available in mobile apps.

To combat this, it is generally advised to use the "suspend" keyword instead to utilise the benefits of coroutines.

However, if you are using Kotlin for a Web application, you can use runBlocking to utilise the power of coroutines as there would be multiple threads available at hand, which is necessary in some use cases.

"suspend" keyword, especially while calling the client APIs will cause cause in 403 error (service unavailable) in certain cases.

How does "suspend" keyword work?

To explain simply, suspend keyword makes use of already spawned coroutines. In Kotlin, there are only limited number of coroutines that can be spawned. suspend does not explicitly spawn new coroutines.

When could you receive 403 error while using "suspend" in Kotlin?

Let's say your app has an API which uses suspend keyword, which internally calls other client API. If your app's traffic is too heavy and there are lot of requests for this API, all the spawned coroutines are already put to other expensive tasks and at the moment if no current tasks are available at hand, after 20 seconds (Standard max. limit for API response time), then it will give us the 403 service unavailable.

Here is an example given below:

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.awaitBody

@SpringBootApplication
class DemoApplication

fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args)
}

data class SecondApiResponse(val data: String)

@Service
class ApiService {

    private val webClient = WebClient.create("URL_TO_SECOND_API")

    suspend fun makeSecondApiCall(): SecondApiResponse {
        return webClient.get()
            .retrieve()
            .awaitBody()
    }
}

@RestController
class ApiController(private val apiService: ApiService) {

    @GetMapping("/api/first")
    suspend fun makeFirstApiCall(): String {
        val secondApiResponse = apiService.makeSecondApiCall()
        return "Response from the first API: ${secondApiResponse.data}"
    }
}

If makeFirstApiCall faces heavy traffic and there are no coroutines to use at the moment, then the response will be 403 if makeSecondApiCall takes more time to execute.

There is no clear cut solution for this as the solution has to be tailored according to the use case.