Performance
ktx is benchmarked end-to-end against the official kotlin CLI (which dispatches .main.kts through the same kotlin-main-kts host that ktx wraps) using hyperfine on the same scripts under identical conditions.
Three configurations are compared in every scenario:
ktx-daemon—ktx run --daemon, the recommended dev workflow. The daemon keeps the embedded Kotlin compiler resident, so subsequent runs only pay the script-compile + class-load cost.ktx—ktx runwithout the daemon. Cold JVM each invocation, but with ktx's compiled-script cache and AppCDS archive both warm.main-kts— systemkotlin foo.main.kts. The straightforward way most people run.ktstoday.
Hardware
All numbers below were measured on Apple M3 (8-core), Temurin JDK 21.0.11, with each tool's caches warmed once before timing. Reproduce locally with mise run bench (script in benchmarks/).
Comparison vs main-kts
Last updated: 2026-06-01 · Source: benchmarks/results.json
What the rows show
Warm — hello.kts (no deps)
The simplest possible script; the goal is to isolate JVM-startup + script-execution overhead with everything else cached.
- ktx-daemon ≈ 248 ms — pure IPC cost, no compiler cold start.
- ktx (no daemon) ≈ 489 ms — JVM start + AppCDS-warm class load + script compile cache hit.
main-kts≈ 1822 ms — JVM start + full scripting host init even with cache hit.
Warm — script with @file:DependsOn
Same protocol but the script imports jackson-databind. All three tools have the dependency in their local Maven cache.
The gap holds: ktx-daemon stays at ~441 ms, while main-kts still has to walk through scripting-host init even when the compiled-script cache is hot.
Dev loop — script edited every run
The hardest case: every run sees a fresh script source, so the on-disk compiled-script cache misses every time. This is the real-world experience of editing a script and pressing Enter.
This row is the headline. ktx-daemon stays around 684 ms because the compiler is already warm in the daemon JVM; ktx without a daemon and main-kts both pay the full embedded-Kotlin-compiler initialization on every invocation.
Cold start — caches cleared
Adversarial baseline: every cache (~/.cache/ktx/compiled, ~/.cache/main.kts.compiled.cache, ktx daemons) is wiped before each run.
ktx still edges out main-kts here, but the margin is small and the daemon row pays a real cost: every measurement re-forks the daemon JVM and re-initializes the embedded compiler from scratch. This is the row to ignore for everyday usage — the daemon's whole point is amortization across runs, not single-shot speed.
The daemon's cold-fork case (~2527 ms with AppCDS warm; longer on first-ever fork) is paid once per daemon lifetime, not per script — see Daemon Mode for the lifecycle details.
Where the time goes
A daemon-warm run is dominated by:
- CLI front-end JVM start + class load — the bulk of it, AppCDS-warm.
- IPC round trip + arg parsing — milliseconds.
- Script class load + execution — depends on the script, usually small.
For a daemon-warm run with a fresh script (compile cache miss), add script compilation against the warm compiler, which is the part the daemon is built to amortize.
Compare to main-kts: a fresh JVM has to redo the embedded-compiler init on every invocation, which is why even cache-hit runs land in the multi-second range.
When ktx is not faster
ktx is faster than main-kts in every scenario the benchmark suite measures, but the margins differ. Two cases where it matters:
- First-ever cold start of the daemon: forking a fresh daemon JVM and initializing the embedded compiler takes ~2 s before any script runs. The cold-start row in the chart above re-forks the daemon every iteration, which is honest about that cost. In real usage you pay this once per daemon lifetime; subsequent runs are warm-row speeds.
- CI one-shot invocations: if a CI job runs a script exactly once and exits, the daemon's amortization buys you nothing and the daemon-fork cost is pure overhead. Use
--frozen(no daemon) instead — lockfile-frozen runs skip Maven resolution and start in the high-hundreds-of-ms range. See Lockfiles.
For frequently-run scripts (dev loop, cron jobs), the daemon's amortization wins overwhelmingly: the dev-loop row shows roughly 6.8× faster than main-kts.
Reproducing
# Build ktx
mise run build
export PATH="$PWD/modules/cli/build/install/ktx/bin:$PATH"
# Install hyperfine if missing
brew install hyperfine # or `mise install hyperfine`
# Run the same suite documented above
mise run benchThe script at benchmarks/bench.sh drives hyperfine over four scenarios and writes benchmarks/results.json, which the chart on this page reads at build time.