How do you choose between Ktor's Netty, CIO and Jetty server engines for a service?
answer
- Engine is chosen at the entry point only
- One engine is pure Kotlin, no Java library
- Two of them are servlet containers
- Handlers and plugins do not change
basics
~20 sNetty is the default and the safest production choice on the JVM. CIO is Ktor's own pure-Kotlin engine, the one that works on non-JVM targets and keeps dependencies minimal. Jetty and Tomcat exist mainly for servlet-container compatibility.
solid answer
~50 sAll engines implement the same Ktor surface, so the choice does not change your routing or plugin code — you swap the factory argument or the `EngineMain` main class. **Netty** is the default in Ktor's own templates: mature, widely deployed, and the best-supported option for a standalone JVM service. **CIO** (Coroutine-based I/O) is written in Kotlin with coroutines end to end, has the smallest dependency footprint, and is the engine available beyond the JVM — it is the natural pick for lightweight or multiplatform builds. **Jetty** and **Tomcat** matter when you need servlet-container semantics or must deploy into an existing container as a WAR rather than as a fat jar. In practice most teams take Netty unless a specific constraint — dependency size, target platform, servlet deployment — pushes them elsewhere, and they benchmark rather than assume, because engine choice is rarely the bottleneck compared to handler work.
code
kotlin · 5 lines// Netty
embeddedServer(Netty, port = 8080) { module() }.start(wait = true)
// Same application, CIO engine
embeddedServer(CIO, port = 8080) { module() }.start(wait = true)go deeper
Recall that the engine is an argument at startup and that Netty is the default; know that the same handlers run on any engine.
Explain what distinguishes the pure-Kotlin engine from the Java-library-backed ones, and name the servlet-container case for Jetty or Tomcat.
Drive the decision from constraints — deployment target, platform targets, dependency budget — and refuse to treat the engine as a performance knob before profiling the handlers.
Standardize the default across services and confine engine-specific configuration to the entry point, so an engine change stays a reversible one-line experiment rather than a migration.
## One application, several engines Ktor separates the *application* (your modules, routing and plugins) from the *engine* (the thing that owns sockets and turns bytes into calls). The engine is chosen at the entry point and nowhere else: ``` embeddedServer(Netty, port = 8080) { module() } embeddedServer(CIO, port = 8080) { module() } ``` or, with file-based startup, by pointing the main class at `io.ktor.server.netty.EngineMain` versus `io.ktor.server.cio.EngineMain`. Each engine ships as its own artifact, so the dependency you add and the main class you name must agree. Nothing inside a module function changes. ## The candidates **Netty.** An established asynchronous JVM networking framework. It is what Ktor's generators produce by default, it is the most exercised path in the ecosystem, and it handles high concurrency well because it is event-loop based rather than thread-per-connection. Its cost is a real dependency tree and a JVM-only story. **CIO.** "Coroutine-based I/O" — Ktor's own engine, implemented in Kotlin on coroutines rather than wrapping a Java networking library. Two properties make it attractive: a much smaller dependency footprint, and availability outside the JVM, since it is not tied to a Java-only library. That makes it the engine of choice for multiplatform projects and for small services where you want the artifact lean. **Jetty** and **Tomcat.** Both are servlet containers wrapped as Ktor engines. Choose one when the deployment target is a servlet container, when you must produce a WAR for an existing platform, or when an operational requirement is expressed in that ecosystem's terms. For a greenfield standalone service they add a layer you are not otherwise using. ## How to actually decide Ask four questions in order. 1. **What is the deployment target?** If it must be a WAR into someone's container, the servlet-backed engines answer the question for you. If it is a container image running a fat jar — the common case — that constraint is absent. 2. **What platforms must this compile for?** JVM-only leaves everything open. Anything broader points at CIO, since the Java-library-backed engines do not follow you off the JVM. 3. **Does artifact size or dependency surface matter?** Serverless-style deployments and libraries care; a long-lived service usually does not. 4. **Only then, performance.** Engines differ, but for a typical service the dominant costs are database round trips, serialization and blocking work accidentally performed on an I/O dispatcher. Swapping engines to fix a latency problem you have not profiled is a classic misdiagnosis. ## Things that do not change with the engine The routing DSL, the plugin set, content negotiation, and how you write handlers are engine-independent — that is the whole point of the abstraction. What *can* differ is engine-specific configuration: connector setup, TLS wiring, and tuning knobs are exposed per engine, so a service that hard-codes those in code has an engine-shaped coupling in its entry point even though the rest is portable. Keeping engine configuration confined to the `main` function or the config file is what makes an engine swap a one-line experiment. One more separation worth stating explicitly, because the naming invites confusion: Ktor's **client** also has engines (CIO, Apache, Java, OkHttp, and others), and they are a completely independent choice. A service can run on the Netty server engine while its outbound `HttpClient` uses the CIO client engine. The two lists overlap in name only. ## What a strong answer sounds like "Netty by default; CIO when I want a minimal dependency set or non-JVM targets; Jetty or Tomcat only when servlet deployment is a requirement. The choice is a one-line change because the application layer is engine-agnostic, so I would benchmark the real workload before treating the engine as a performance lever." That answer shows you know the options are real alternatives *and* that the engine is rarely the interesting variable.
- If you switch a Ktor service from Netty to CIO, what code has to change?The engine dependency, and the factory argument to embeddedServer or the EngineMain main class. Engine-specific connector or TLS configuration written in the entry point may need rewriting. Modules, routing and plugins are untouched, which is why the swap is cheap enough to benchmark rather than argue about.
- A team blames the engine for high tail latency. What would you check first?Whether handlers block the I/O dispatcher — synchronous JDBC calls, file I/O or CPU-heavy work inside a suspending handler starve the event loop regardless of engine. Then downstream call latency and serialization cost. Engine differences usually sit far below those in the profile.
- Does the server engine choice constrain which client engine the same service can use?No. Ktor's server engines and HttpClient engines are separate abstractions with separate artifacts; a Netty-based server can use any client engine, and vice versa. The shared name CIO across both lists is the only thing that suggests otherwise.
saying these in an interview costs you the question
- Claims switching engines requires rewriting routing code
- Says CIO means 'blocking I/O'
- Treats an engine swap as the standard fix for slow endpoints
- Believes the servlet-backed engines are required for HTTP/2 or TLS
- Conflates server engines with HttpClient engines