Ktor(読み方はケイター、公式表記は /keɪ-tor/)は、JetBrainsが開発するKotlin向けの非同期Webフレームワークです。サーバーとHTTPクライアントの両方を同じ設計思想で書け、ライセンスはApache-2.0です。2026年10月時点の最新版は3.6.0(2026年9月16日公開)です。
この記事では、3.0から3.6までの変更点、エンジンごとのスレッドモデル、ブロッキング処理が同時接続を詰まらせる条件を実測値とあわせて整理します。後半では、3.6.0で実際にビルドしたREST APIとテストのコード、Spring Bootとの選び分けを示します。
まとめ:Ktor 3.6の要点と採用判断
- 最新版は3.6.0(2026年9月16日)。リポジトリのビルド設定ではKotlin 2.3.21を使っており、3.6.0の型安全な認証APIだけはKotlin 2.4.0以上(または
-Xcontext-parameters)が必要です。 - 本体の最小JDKは8ですが、Jetty Jakartaエンジンは17、Tomcat Jakartaエンジンは11を要求します。新規ならJDK 17以上に揃えると迷いません。
- ハンドラは
suspend関数として動きます。Nettyエンジンでは処理スレッド数の既定値がCPU数なので、Thread.sleepやJDBCのような待ちをそのまま書くと、処理スレッドが埋まって後続のリクエストが待たされます。16コア機での実測では、1秒待つハンドラへの同時100リクエストがdelayなら1.89秒、Thread.sleepなら8.02秒かかりました。 - 機能は
install()で1つずつ足します。3.6.0のサーバープラグインは46モジュールあり、OpenID Connect(実験的)と型安全な認証API(実験的)が加わりました。 - Spring Data・Spring Securityの資産に頼る業務システムならSpring Boot、Kotlin MultiplatformでクライアントとAPIを共有する構成や小さなAPI・BFFならKtorが向きます。
Ktorの位置づけと名前の由来
Ktorは2015年にGitHubでリポジトリが作られ、1.0が2018年11月19日に公開されました。名前は公式FAQによると、コンストラクタの略語 ctor の頭文字をKotlinの「K」に置き換えたものです。2026年10月時点でGitHubのスター数は約1万4,500です。
一つのプロジェクトで次の2つを提供している点が、他のJVM向けフレームワークとの大きな違いです。
- Ktor Server:ルーティング、JSON変換、認証、WebSocket、Server-Sent EventsなどをプラグインとしてAPIサーバーに足していく仕組み
- Ktor Client:JVM・Android・iOS・JavaScript・Wasmで同じコードを使えるHTTPクライアント。AndroidではRetrofitの代わりに、Kotlin Multiplatformでは共通コードの通信層として使われます
言語仕様やコンパイラの現状はKotlinとは?Javaとの違いとK2コンパイラ・2.4系の現在地で扱っています。
Ktor 3.0〜3.6の版履歴と主な変更点
3系の大きな区切りは3.0.0(2024年10月9日)です。I/O基盤が独自実装からkotlinx-ioに移り、2系で非推奨だった withTestApplication と Locations プラグインが削除されました(型安全なルーティングは Resources プラグインへ移行)。マルチパートで受け取るファイル項目には、既定で50MiBの上限も入りました。以降はおおむね年3回のマイナー版で機能を足しています。
表の公開日は、GitHub Releasesのリリース本文にある「Published」の日付です。3.6.0は公式ドキュメントの変更点ページでは9月17日と書かれており、出典によって1日ずれます。
| 版 | 公開日 | 主な追加・変更 |
|---|---|---|
| 3.0.0 | 2024-10-09 | kotlinx-io移行・旧テストAPI削除 |
| 3.2.0 | 2025-06-12 | DIプラグイン・suspendモジュール |
| 3.3.0 | 2025-09-11 | TLSなしHTTP/2(h2c)・Jetty 12 |
| 3.4.0 | 2026-01-22 | OpenAPI仕様の生成・zstd・APIキー認証 |
| 3.5.0 | 2026-05-14 | Kotlin 2.3.21・パラメータ検証関数 |
| 3.6.0 | 2026-09-16 | OIDC・型安全な認証・Netty HTTP/3 |
運用中の案件で注意したいのは3.4.3です。このパッチではNettyエンジンがリクエストを断続的に取りこぼす不具合(KTOR-9531)と、ハンドラが本来とは別のイベントループで動く不具合(KTOR-9542)が報告され、3.5.0で修正されました。3.4.3で止めているなら、3.5系以降へ上げる理由になります。
3.6.0の主な追加は次のとおりです。詳細と実験的APIの利用条件は公式のWhat's new in Ktor 3.6.0で確認できます。
- OpenID Connectプラグイン(実験的・JVMのみ):発行者URLを登録するだけで、ディスカバリー、JWKS取得、JWT検証、PKCE付き認可コードフローをまとめて設定できます
- 型安全な認証API(実験的):プロバイダを文字列名で参照せず、スキームの値をルートに渡します。保護されたルート内の
call.principalは非nullの型になります - NettyのHTTP/3(実験的):SSLコネクタを設定したうえで
enableHttp3()を呼ぶとQUICで待ち受けます。同じサーバーでh2cとTLS上のHTTP/2も併用できるようになりました receiveのnull許容:receiveNullable()が非推奨になり、call.receive<Payload?>()と書くようになりました- クライアントの既定エンジン:新しい成果物
ktor-client-engine-defaultsを共通コードに入れると、エンジンを指定せずHttpClient()でプラットフォームごとの既定エンジンが選ばれます
コルーチンで動くリクエスト処理とエンジン別のスレッドモデル
suspend関数のハンドラとパイプラインの関係
Ktorのルートハンドラは suspend ラムダです。delay() や非同期のHTTP呼び出しで待つ間はスレッドを手放すので、少ないスレッドで多くの接続をさばけます。プラグインはリクエスト処理の段階(フェーズ)に割り込む形で実装されています。install(ContentNegotiation) を呼ぶとJSONの読み書きが、install(StatusPages) を呼ぶと例外からレスポンスへの変換が、パイプラインに差し込まれます。コルーチンそのものの仕組みはKotlin Coroutinesとは?suspendの仕組みと構造化並行性・採用判断を参照してください。
エンジン4種の対応プラットフォームとJDK要件
サーバーを動かすエンジンは差し替えられます。公式の対応表と、3.6.0タグのビルド設定から読み取れる要件は次のとおりです。
| エンジン | 動く環境 | HTTP/2 | JDKの下限 |
|---|---|---|---|
| Netty | JVM | 対応 | 8(本体と同じ) |
| Jetty(Jakarta) | JVM | 対応 | 17 |
| Tomcat(Jakarta) | JVM | 対応 | 11 |
| CIO | JVM・Native・GraalVM・JS・Wasm | 非対応 | 8(本体と同じ) |
JDKの下限は、ビルド設定の既定値 DEFAULT_JDK = 8 と、Jetty Jakartaの jvmToolchain(17)、Tomcat Jakartaの jvmToolchain(11) から読み取ったものです。3.6.0が依存するのはNetty 4.2.17、Jetty 12.1.13、Tomcat 10.1.59です。CIOはKotlinだけで書かれたエンジンで、Kotlin/NativeやGraalVMのネイティブイメージにしたい場合の選択肢になります。
ハンドラを実行するスレッド:Netty=CPU数、CIO=Dispatchers.IO
同じハンドラでも、エンジンによって実行されるスレッドプールが違います。3.6.0のソースでは、Nettyはハンドラを callGroupSize 本のイベントループで動かし、その既定値は parallelism(利用可能なCPU数)です。CIOはJVMではハンドラを Dispatchers.IOで動かします。JVMでの既定の並列実行上限は、64と利用可能なCPU数の大きい方です。待ちのない処理ならこの違いは表に出ません。違いが出るのは、スレッドを握ったまま待つ処理を書いたときです。
ブロッキング処理が同時接続を詰まらせる条件(3.6.0で実測)
1秒待つだけのハンドラを3通りに書き、xargsの並列数を100に設定してcurlを実行し、全100リクエストの完了時間を測りました。100件が厳密に同時刻に到着する条件ではありません。環境はIntel Mac(16コア)、JDK 25、Kotlin 2.4.21、Ktor 3.6.0です。この測定値には、curlのプロセス起動、接続確立、リクエスト送受信などの時間も含まれます。ハンドラの待ち時間以外の内訳は測定していません。
get("/wait/delay") { delay(1000); call.respondText("delay") }
get("/wait/sleep") { Thread.sleep(1000); call.respondText("sleep") }
get("/wait/io") {
withContext(Dispatchers.IO) { Thread.sleep(1000) }
call.respondText("io")
}
| 書き方 | Netty | CIO |
|---|---|---|
| delay(中断) | 1.89秒 | 2.01秒 |
| Thread.sleep(ブロック) | 8.02秒 | 2.71秒 |
| Dispatchers.IOで包む | 2.76秒 | 2.63秒 |
Nettyで Thread.sleep を書くと、16本の処理スレッドが全部埋まり、残りのリクエストは順番待ちになります。均等に振り分けられる単純モデルでは、100件を16本で処理すると最大7回分の待機が必要です。ただし、実測8.02秒の内訳をこの計算だけで確定することはできません。CIOが2.71秒で済んだのは、ハンドラが Dispatchers.IO で動くためです。この16コア環境での既定の並列実行上限は64です。スレッドは必要に応じて作成されるため、最初から64本が常駐しているわけではありません。それでも、ブロッキング処理が同時に64枠を占有すれば、追加の処理は待たされます。接続数が64を超えるだけで詰まるわけではありません。
実務で問題になるのは Thread.sleep ではなく、JDBCドライバでのDBアクセス、同期型のファイルI/O、ブロッキングするSDKの呼び出しです。対処の優先順は次のとおりです。
- 非同期APIがあるライブラリ(Ktor ClientやR2DBCなど)に置き換え、
suspendのまま待つ - 置き換えられない呼び出しは
withContext(Dispatchers.IO) { ... }で包み、Nettyの処理スレッドから外す - それでも足りなければ、本文のような
embeddedServer構成ではconfigure = { callGroupSize = 32 }のようにコードで調整します。EngineMainで設定ファイルを読む構成ではktor.deployment.callGroupSizeを指定します。ただし、待ちをスレッド数で吸収する発想に戻るので最後の手段です
「コルーチンだから速い」とは言えません。ハンドラの中身が中断可能かどうかで決まります。JDBCのような待ちが多い処理をスレッドで並べる方式との比較はJava 仮想スレッドとは?プラットフォームスレッドとの違い・仕組みと効く処理を実測で解説が参考になります。
最小構成のREST APIを3.6.0で動かす手順
依存関係の指定(GradleとMavenの違い)
公式の雛形はGradle(Kotlin DSL)です。新規作成はWebの start.ktor.io か、IntelliJ IDEAのKtorプロジェクト作成から行います。手で書く場合、必要な依存は次のとおりです(本記事でMavenを使って検証した依存を、Gradle表記に直したものです)。
plugins {
kotlin("jvm") version "2.4.21"
kotlin("plugin.serialization") version "2.4.21"
}
repositories {
mavenCentral()
}
val ktorVersion = "3.6.0"
dependencies {
implementation("io.ktor:ktor-server-netty:$ktorVersion")
implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
implementation("io.ktor:ktor-server-status-pages:$ktorVersion")
implementation("ch.qos.logback:logback-classic:1.5.18")
testImplementation("io.ktor:ktor-server-test-host:$ktorVersion")
testImplementation(kotlin("test"))
}
MavenではGradleのモジュールメタデータを解決しないため、ktor-server-core-jvm のように末尾に -jvm が付いた成果物名を指定します。本記事の検証はMaven 3.9.11でこの形にして行いました。JSONの変換には、kotlin-maven-pluginにkotlinx-serializationのコンパイラプラグインを追加する必要があります。
ルーティング・JSON変換・例外処理を備えたアプリ本体
書籍を登録して取得するだけのAPIです。保存先はメモリ上のMapなので再起動するとデータが消え、同じIDをPOSTすると既存の書籍を上書きします。ContentNegotiation がデータクラスとJSONを相互変換し、StatusPages が独自例外を404に変換します。ファイルは src/main/kotlin/example/App.kt に置きます。サーバー用の ContentNegotiation は io.ktor.server.plugins.contentnegotiation パッケージにあり、後述するクライアント用とは別のクラスです。
package example
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.Netty
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import java.util.concurrent.ConcurrentHashMap
@Serializable
data class Book(val id: Int, val title: String)
@Serializable
data class ErrorBody(val error: String)
class BookNotFound(id: Int) : RuntimeException("book $id not found")
fun Application.module() {
install(ContentNegotiation) { json() }
install(StatusPages) {
exception<BookNotFound> { call, e ->
call.respond(HttpStatusCode.NotFound, ErrorBody(e.message ?: "not found"))
}
}
val books = ConcurrentHashMap<Int, Book>()
routing {
route("/books") {
post {
val book = call.receive<Book>()
books[book.id] = book
call.respond(HttpStatusCode.Created, book)
}
get("/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
?: return@get call.respond(HttpStatusCode.BadRequest, ErrorBody("id must be int"))
call.respond(books[id] ?: throw BookNotFound(id))
}
}
}
}
fun main() {
embeddedServer(Netty, port = 8080, module = Application::module).start(wait = true)
}
エンジンをCIOに替えるときは、依存を ktor-server-cio にし、embeddedServer(CIO, ...) と書き換えるだけです。ルーティングのコードは変わりません。
testApplicationで書く結合テスト
3.0で旧APIが削除されたため、テストは testApplication で書きます。ポートを開かずにアプリを起動し、付属のクライアントで呼び出します。ファイルは src/test/kotlin/example/AppTest.kt です。
package example
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.testing.*
import kotlin.test.*
class AppTest {
@Test
fun createAndGet() = testApplication {
application { module() }
val created = client.post("/books") {
contentType(ContentType.Application.Json)
setBody("""{"id":1,"title":"Ktor"}""")
}
assertEquals(HttpStatusCode.Created, created.status)
assertEquals("""{"id":1,"title":"Ktor"}""", client.get("/books/1").bodyAsText())
assertEquals(HttpStatusCode.NotFound, client.get("/books/2").status)
assertEquals(HttpStatusCode.BadRequest, client.get("/books/abc").status)
}
}
このテストは mvn test で成功しました。実サーバーをcurlで叩いた結果は次のとおりです。
$ curl -X POST -H 'Content-Type: application/json' -d '{"id":7,"title":"Kotlin"}' localhost:8080/books
{"id":7,"title":"Kotlin"} # 201
$ curl localhost:8080/books/9
{"error":"book 9 not found"} # 404
Ktor Clientによる書籍APIの呼び出し
クライアント例には、前掲のJSON変換用依存に加え、implementation("io.ktor:ktor-client-cio:$ktorVersion") と implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") が必要です。クライアント用の ContentNegotiation はサーバー用とは別のクラスです。ファイルは src/main/kotlin/example/Client.kt とし、サーバー(AppKt)を起動した状態で別プロセスとして実行します。サーバーと同じデータクラスをそのまま送受信できるのが、Kotlinで両側を書く利点です。
package example
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
HttpClient(CIO) {
install(ContentNegotiation) { json() }
}.use { client ->
client.post("http://localhost:8080/books") {
contentType(ContentType.Application.Json)
setBody(Book(3, "Coroutines"))
}
val book: Book = client.get("http://localhost:8080/books/3").body()
println(book) // Book(id=3, title=Coroutines)
}
}
Kotlin Multiplatformで共通コードに置く場合は、エンジンを指定せず、3.6.0の ktor-client-engine-defaults に選ばせる構成が公式の推奨です。KMP自体の採用判断はKotlin Multiplatform(KMP)とFlutter・React Native比較|導入判断で比較しています。
Spring Bootとの違いとKtorの採用判断
| 観点 | Ktor 3.6 | Spring Boot |
|---|---|---|
| 機能の足し方 | install()で明示 | 依存追加で自動構成 |
| 非同期の書き方 | コルーチン | Servlet・WebFlux・仮想スレッド |
| DI | 3.2以降の組み込みDI | Springコンテナ |
| JVM以外 | CIOでNative・JS・Wasm | JVM(ネイティブはGraalVM) |
| データアクセス | Exposed等を自分で選ぶ | Spring Data |
Ktorは、導入するプラグインとルーティングをコードで明示的に組み立てるフレームワークです。install() していないプラグインは動かないので、挙動を追いやすい代わりに、CORS、認証、ログ、メトリクスを自分で選んで組む手間がかかります。Spring Bootの依存名や最小構成はSpring Bootとは|4.1系の依存名の変更と最小Web APIの作り方と見比べると差がわかります。
Ktorを選ばない方がよい場面
- Spring Security、Spring Data JPA、Spring Batchなど、Springの部品を前提に設計された業務システム。Ktorで同等の構成を組むと、選定と結線を自前で抱えることになります
- チームの大半がJava中心で、
suspendとブロッキング呼び出しの区別を付けられない場合。上の実測が示すように、ブロッキング処理がNettyのハンドラ実行スレッドを占有すると、追加のリクエスト処理が待たされます - OIDCや型安全な認証APIに本番の認可を載せたい場合。3.6.0ではどちらも
@ExperimentalKtorApiで、APIが変わる前提です。安定版のinstall(Authentication)とJWTプラグインで組む方が安全です
Ktorが合う場面
- Kotlin Multiplatformのアプリと、データクラスや通信コードを共有するAPIサーバー
- モバイル向けBFFや社内向けの小さなAPIのように、必要なプラグインを限定し、ルーティングと構成を明示的に管理したい構成
- AndroidやKMPのHTTPクライアント(サーバーをKtorにしなくても使えます)
よくある質問
Ktorの読み方は?
公式FAQの表記は /keɪ-tor/ で、日本語では「ケイター」と読みます。名前はコンストラクタの略語 ctor に由来します。
Ktorの最新バージョンはいくつですか?
2026年10月時点では3.6.0(2026年9月16日公開)です。直前の版は3.5.2(2026年7月31日)です。最新はGitHubのReleasesかMaven Centralで確認してください。
KtorはどのJavaバージョンで動きますか?
本体の最小JDKは8です。ただし、Jetty Jakartaエンジンは17、Tomcat Jakartaエンジンは11が必要です。NettyかCIOを使うなら8でも動きますが、新規開発ではJDK 17以上を選ぶのが無難です。
NettyとCIOはどちらを選べばよいですか?
JVMでHTTP/2やHTTP/3(実験的)を使うならNettyです。Kotlin/NativeやGraalVMでネイティブ化したい場合や、依存を減らしたい場合はCIOを選びます。CIOはHTTP/2に対応していません。ブロッキング処理を含むハンドラでは、スレッド数の違いで挙動が変わる点にも注意してください。
Spring BootからKtorへ移行すべきですか?
Springの部品に依存していないAPIで、Kotlinに統一したい場合は候補になります。Spring SecurityやSpring Dataを使っているなら、置き換えのコストが利点を上回ることが多いので、新規の小さなサービスから試す方が安全です。