What is a Ktor client engine, and how do you choose one for JVM, Android, or iOS?
answer
- Core has the API, not the transport
- Chosen by dependency, named at construction
- One API, per-platform implementations
- CIO, OkHttp, Java, Darwin, Js, Curl
- engine { } block is engine-specific
basics
~20 sA Ktor client engine is the pluggable implementation that actually performs requests. HttpClient is the shared API; the engine comes from a separate artifact passed as HttpClient(CIO), HttpClient(OkHttp), HttpClient(Darwin), and differs per platform and capability.
solid answer
~40 s`ktor-client-core` gives you the API — the request DSL, plugins, `HttpResponse` — but contains no transport. The transport is an *engine*, added as its own dependency and passed to the constructor: `HttpClient(CIO)`, `HttpClient(OkHttp)`, `HttpClient(Java)`, `HttpClient(Apache5)`, `HttpClient(Jetty)` on the JVM; `Android` or `OkHttp` on Android; `Darwin` on iOS/macOS; `Js` in the browser; `Curl`/`WinHttp` on other native targets. That split is what makes the client multiplatform: shared code writes one set of calls and each target supplies its own engine. `HttpClient()` with no argument resolves an engine from the artifacts on the classpath. Engine-specific settings live in an `engine { }` block — `HttpClient(CIO) { engine { maxConnectionsCount = 1000 } }`. Engines differ in capability (HTTP/2 and WebSocket support in particular), so check Ktor's engine table before relying on a feature.
code
kotlin · 12 lines// Same configuration, different transports
val cio = HttpClient(CIO) {
engine {
maxConnectionsCount = 1000
endpoint { maxConnectionsPerRoute = 100 }
}
}
val okhttp = HttpClient(OkHttp)
// Resolves whichever engine artifact is on the classpath
val defaultEngine = HttpClient()go deeper
Recall that the engine is a separate dependency passed to the HttpClient constructor, and be able to name at least one — CIO or OkHttp — plus what happens if you forget it.
Explain the split between ktor-client-core and the engine artifacts, why that split is what makes the client multiplatform, and what belongs in the engine { } block versus the plugin configuration.
Show that you check capability before committing: WebSocket and HTTP/2 support vary by engine, and a missing capability surfaces at runtime. Justify a choice by the surrounding stack, not by folklore about speed.
Own the standardisation question: one engine per platform across services, engine-agnostic configuration wherever possible, and a clear rule for when a team may deviate — because engine choice leaks into dependency footprint and supportability.
## What an engine is A Ktor `HttpClient` is two layers. The upper layer — the request DSL, the plugin pipeline, `HttpResponse`, converters — lives in `ktor-client-core` and is the same everywhere. The lower layer is an **engine**: the component that opens the connection, writes the request bytes and reads the response bytes. The engine is chosen by dependency and named at construction time: val client = HttpClient(CIO) `CIO` here is an *engine factory* object from the `ktor-client-cio` artifact. Adding `ktor-client-core` alone gives you code that compiles but cannot run. ## Choosing by platform The engine list is per-target, and this is the core of the multiplatform story: - **JVM/server:** `CIO` (pure Kotlin, coroutine-based), `Java` (the JDK's own HTTP client), `OkHttp`, `Apache5`, `Jetty`. - **Android:** `Android`, `OkHttp`, `CIO`. - **iOS / macOS (Kotlin/Native Apple targets):** `Darwin`. - **Browser / Node (Kotlin/JS):** `Js`. - **Other native targets:** `Curl`, and `WinHttp` on Windows. In a Kotlin Multiplatform project the usual arrangement is: shared code in `commonMain` depends on `ktor-client-core` and builds requests against the common API, while each platform source set adds its own engine artifact. The shared code either calls the no-argument `HttpClient { ... }` or obtains a platform client through an `expect`/`actual` factory function. ## Choosing on the JVM When several engines are available, the decision is usually about the surrounding stack rather than raw speed: - `CIO` has no external dependency and is coroutine-native, which makes it the default choice for a Ktor-only service. - `OkHttp` is attractive when the team already runs OkHttp interceptors, or on Android where it is the house client. - `Java` reuses the JDK client, keeping the dependency footprint minimal. - `Apache5` and `Jetty` suit teams standardised on those stacks or needing their specific tuning knobs. ## The default engine `HttpClient()` with no factory resolves an engine from what is on the classpath rather than hard-coding one. It is convenient for shared code and for samples, but it makes the choice implicit — if no engine artifact is present the failure appears at runtime, and if two are present you may not be able to tell from the call site which one is in use. Naming the engine explicitly is the clearer default for a single-platform service. ## Engine-specific configuration The generic client options (plugins, `expectSuccess`, `defaultRequest`) are engine-agnostic. Anything specific to the transport goes in the `engine { }` block, whose type depends on the engine: HttpClient(CIO) { engine { maxConnectionsCount = 1000 endpoint { maxConnectionsPerRoute = 100 } } } Engines also expose a `proxy` setting built with `ProxyBuilder`, and some (notably `OkHttp`) accept a `preconfigured` instance of their own native client so an existing configuration can be reused. ## Capability differences Engines are not feature-identical. Support for HTTP/2 and for WebSockets in particular varies by engine, and Ktor documents this in an engine support table. This matters in practice: installing the `WebSockets` plugin on an engine that does not support the protocol fails at runtime, not at compile time. When a client feature is essential, verify engine support before choosing — and be aware the table changes across Ktor releases as engines gain capabilities. ## What does not change Swapping engines does not change your request code. `client.get(...)`, the builder block, plugins, converters, response handling — all identical. What changes is the dependency, the `engine { }` block's contents, and the capability set. That is the point of the abstraction: the same integration code compiles for a server, an Android app and an iOS app, and each gets a transport appropriate to its platform. ## Common mistakes Adding `ktor-client-core` and expecting requests to work; assuming a JVM engine is available on a native target; assuming every engine supports WebSockets; and pushing engine-level tuning into the wrong place — timeouts, for instance, are usually best expressed with the engine-agnostic `HttpTimeout` plugin rather than duplicated per engine.
- What happens if you call HttpClient() with no engine argument?Ktor resolves an engine from the engine artifacts present on the classpath. It is handy in shared multiplatform code, but the choice becomes implicit: with no engine artifact the call fails at runtime, and with several it is not obvious at the call site which transport is active.
- Does changing the engine change the code that issues requests?No. The request DSL, plugins, converters and response handling all live above the engine and are unchanged. What changes is the dependency you declare, the contents of the engine { } block, and the capability set — HTTP/2 and WebSocket support in particular vary by engine.
- Where should request timeouts be configured — the engine or a plugin?Prefer the engine-agnostic HttpTimeout plugin, which exposes requestTimeoutMillis, connectTimeoutMillis and socketTimeoutMillis and behaves the same whichever engine is underneath. Engine blocks can express transport-specific limits, but duplicating timeouts there makes behaviour depend on the engine you happen to have chosen.
The engine is like a printer driver: the document and the print dialog stay the same, but the driver that talks to the hardware is chosen per machine.
saying these in an interview costs you the question
- Thinks ktor-client-core alone can perform requests
- Assumes every engine supports WebSockets or HTTP/2
- Believes the request DSL differs per engine
- Picks an engine on speed claims with no measurement
- Cannot name an engine for a non-JVM target