Kotlin

Ktorとは?Kotlin製Webフレームワークの仕組みと使い方【3.6対応】

Ktorとは?Kotlin製Webフレームワークの仕組みと使い方【3.6対応】

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の呼び出しです。対処の優先順は次のとおりです。

  1. 非同期APIがあるライブラリ(Ktor ClientやR2DBCなど)に置き換え、suspend のまま待つ
  2. 置き換えられない呼び出しは withContext(Dispatchers.IO) { ... } で包み、Nettyの処理スレッドから外す
  3. それでも足りなければ、本文のような 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を使っているなら、置き換えのコストが利点を上回ることが多いので、新規の小さなサービスから試す方が安全です。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.07.09 テックブログ Muse Imageとは?Metaの画像生成AIの使い方・API料金(1枚0.01ドル)と日本での提供状況【2026年10月】
  2. 2026.10.21 テックブログ ユミルリンク不正アクセス第二報で延べ5,162万件:Cuenote FC利用企業と受信者の確認点
  3. 2026.10.23 テックブログ Renta!はいつ再開?不正アクセスでメールアドレス等が流出したおそれ・再開時のパスワード再設定と運営側の対応
  4. 2026.10.23 テックブログ Splunk 脆弱性CVE-2026-76299:影響版の確認と10.2.8などへの更新手順
  5. 2026.10.24 テックブログ ブラストメール不正アクセス:配信先345件の漏えいとAPIサーバー侵入の原因、利用企業の報告・通知の手順

RELATED POSTS 関連記事

目次