Guide
Choosing a Ktor Client Engine for JVM and Multiplatform Projects
Compare CIO, Jetty, Apache, and OkHttp engines in Ktor to pick the best fit for your JVM/Android/iOS/Native project.
Published by Tasadduq Burney
28 Dec 2025, 15:37 UTC
4 min148.7K views0

Decision: Selecting a Ktor Client Engine
When adding a Ktor HTTP client to a JVM, Android, iOS Native or desktop target you must pick an engine. The engine determines blocking behaviour, feature set, binary size and which platforms are supported.
Constraints and Requirements
Identify the non‑functional limits that matter for your project:
- Target platforms (JVM, Android, iOS Native, macOS, Windows, Linux)
- Whether you can tolerate blocking threads or need a fully coroutine‑based, non‑blocking client
- Required protocol features (HTTP/2, WebSockets, advanced authentication, cookie handling)
- Binary size tolerance (especially for Android APKs)
- Dependency policy (preferring to avoid extra third‑party jars)
Engine Comparison
| Engine | Blocking / Thread model | Key Features | Supported Targets | Dependency Impact |
|---|---|---|---|---|
| CIO | Fully non‑blocking, coroutine‑based, stays on the event‑loop thread | HTTP/1.1, HTTP/2 (via ALPN), WebSocket, no extra libraries | JVM, Android, iOS Native, macOS, Windows, Linux | Only Ktor core; no extra artifact |
| Jetty | Blocking thread pool unless Jetty async API is enabled; can be made non‑blocking with proper configuration | HTTP/2 (ALPN or Conscrypt), servlet‑based, mature server‑side reuse | JVM, Android (limited), no Kotlin/Native | Adds Jetty artifacts; increases JAR/APK size |
| Apache | Synchronous API layer over HttpClient 4.x; blocks the calling thread | Extensive redirect handling, multiple auth schemes, cookie store, proxy support | JVM, Android (limited), no Kotlin/Native | Adds Apache HttpClient; notable size increase |
| OkHttp | Non‑blocking dispatchers, uses OkHttp’s connection pool; stays off the main thread on Android | HTTP/2, WebSocket, connection pooling, transparent GZIP, response caching | JVM, Android only (no iOS Native) | Adds OkHttp artifact; moderate size increase |
Trade‑off Summary
- CIO is the default choice for multiplatform projects because it adds no extra dependencies and works everywhere Kotlin/Native runs. Use it when you need a purely coroutine‑driven client and do not require Jetty‑specific servlet features.
- Jetty gives you a battle‑tested servlet container and HTTP/2 via ALPN, but unless you enable Jetty’s async API it will consume a thread per connection. It is unsuitable for iOS Native and adds noticeable binary weight.
- Apache provides the richest set of protocol conveniences (auth schemes, cookie policies) at the cost of a blocking API and larger binaries. Choose it only when you need those specific features and are targeting JVM/Android.
- OkHttp is the de‑facto standard on Android, offering HTTP/2, WebSockets and efficient connection pooling with a small runtime overhead. It cannot be used on Kotlin/Native iOS targets.
Concrete Implementation and Validation
The following snippet shows how to instantiate a Ktor client with each engine and perform a simple GET request to a public test endpoint. Replace <Engine> with the desired engine class.
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.engine.jetty.*
import io.ktor.client.engine.apache.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.request.*
import io.ktor.client.response.*
import io.ktor.http.*
fun testClient(engine: HttpClientEngine) = HttpClient(engine) {
// common configuration: timeout, logging, etc.
expectSuccess = true
}
suspend fun main() {
val engines = listOf(
Pair("CIO", CIO),
Pair("Jetty", JettyEngine),
Pair("Apache", ApacheEngine),
Pair("OkHttp", OkHttp)
)
for ((name, engineFactory) in engines) {
try {
val client = testClient(engineFactory)
val response = client.get("https://httpbin.org/get")
println("$name: status = ${response.status}, body length = ${response.contentLength}")
client.close()
} catch (e: Exception) {
println("$name failed: ${e.message}")
}
}
}
To verify the result:
- Run the code on the JVM desktop target (or Android emulator) using Gradle:
./gradlew run. - Check console output (or Logcat) for each engine line showing a 200 status and a non‑zero body length.
- Confirm no linkage errors appear; if an engine fails to start, the catch block will print the exception.
- Optionally attach a CPU/memory profiler and observe that CIO and OkHttp stay on the event‑loop/netty thread, while Jetty (without async) shows worker‑thread usage.
Limitations
- Jetty’s HTTP/2 requires Jetty‑ALPN or Conscrypt on JDK < 9; otherwise the connection falls back to HTTP/1.1.
- OkHttp’s WebSocket usage may need extra ProGuard rules on Android builds.
- Apache engine brings in HttpClient 4.x, which can conflict with other libraries that depend on newer versions; consider using Gradle’s
dependencyConstraintsto align versions. - CIO does not expose low‑level socket options; if you need fine‑grained TCP tuning, Jetty or Netty‑based engines are preferable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.