Choosing the Right Ktor HTTP Client Engine for Your Kotlin Application
Learn how to pick CIO, Jetty, or Apache engines in Ktor, see a code example, and understand the trade‑offs.
19 Mar 2026, 06:47 UTC

Problem: You need an HTTP client that fits your concurrency model and feature requirements
When building a Kotlin service with Ktor, you must decide which underlying HTTP client engine to use. The engine affects thread usage, blocking behavior, and available features such as authentication schemes or cookie handling. Picking the wrong engine can lead to thread‑starvation, unnecessary dependencies, or missing functionality.
Thesis: By selecting the engine that matches your application’s concurrency style and feature needs, you get a portable HttpClient API while retaining control over performance and capabilities.
Engine Overview
- CIO – coroutine‑based, non‑blocking engine. Ideal for high‑concurrency, reactive services because it uses a small pool of worker threads.
- Jetty – servlet‑based, blocking engine. Provides mature servlet features and can be used when you need Jetty‑specific APIs (e.g., custom SSLContext).
- Apache – based on HttpComponents. Offers extensive protocol support (various auth schemes, redirect handling) but brings a larger dependency footprint.
All three engines implement the same io.ktor.client.HttpClient interface, so your application code stays unchanged when you swap the engine.
Worked Example: Switching Between CIO and Jetty
Assume a Gradle Kotlin DSL project. First, add the Ktor client core and the serialization feature:
dependencies {
implementation "io.ktor:ktor-client-core:2.3.0"
implementation "io.ktor:ktor-client-content-negotiation:2.3.0"
implementation "io.ktor:ktor-serialization-kotlinx-json:2.3.0"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0"
}
To use the CIO engine, add its dependency and configure the client:
dependencies {
implementation "io.ktor:ktor-client-cio:2.3.0"
}
val client = HttpClient {
engine {
// CIO‑specific options: number of worker threads
workerThreads = 4
}
install(ContentNegotiation) {
json()
}
}
suspend fun fetchJson(): MyData = client.get {
url("https://httpbin.org/json")
accept("application/json")
}.body()
To switch to Jetty, replace the CIO artifact with the Jetty one and adjust the engine block:
dependencies {
implementation "io.ktor:ktor-client-jetty:2.3.0"
}
val client = HttpClient {
engine {
// Jetty‑specific options: thread pool size
maxThreads = 200
}
install(ContentNegotiation) {
json()
}
}
The request code (client.get { … }) remains identical. After building and running the program, you can verify the engine in use by logging client.engine.javaClass.simpleName.
Trade‑offs and Limitations
- Performance: CIO typically yields lower latency under high concurrency because it avoids blocking threads. Jetty and Apache may introduce thread‑pool overhead.
- Blocking vs. Non‑blocking: Using Jetty or Apache in a coroutine‑heavy codebase can block the dispatcher if you call client methods directly from
Dispatchers.MainorDispatchers.Default. Mitigate by wrapping calls inwithContext(Dispatchers.IO). - Feature Portability: Engine‑specific capabilities (e.g., Jetty’s
SSLContextsetter or Apache’s cookie store) are not available through the common HttpClient API. If you rely on them, abstract the functionality behind your own interface or keep engine‑specific code in a separate module. - Dependency Size: Apache brings the largest set of transitive dependencies; CIO is the lightest.
Practical way to check the result: run the application with each engine, print client.engine and the response status code. If the status is 200 and the engine name matches the expected one, the configuration is correct.
Actionable Closing
- Identify your concurrency model: reactive/coroutine‑heavy → prefer CIO; need Jetty‑specific APIs or servlet compatibility → choose Jetty; require extensive auth/protocol features → consider Apache.
- Add the corresponding
ktor-client-{engine}artifact to your build. - Configure the engine block with any needed options (worker threads, max threads, SSL context, etc.).
- Keep the request logic engine‑agnostic; isolate any engine‑specific calls behind abstractions.
- Validate by logging the engine class and checking a known endpoint’s response.
Following these steps lets you match the HTTP client engine to your application’s needs without rewriting core client code.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.