Skip to content

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-daemonktx 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.
  • ktxktx run without the daemon. Cold JVM each invocation, but with ktx's compiled-script cache and AppCDS archive both warm.
  • main-kts — system kotlin foo.main.kts. The straightforward way most people run .kts today.

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

ktx-daemon ktx 0.1.0 main-kts Kotlin 2.3.21-release-298
Warm — hello.kts (no deps)
ktx-daemon
248 ms
ktx
489 ms
main-kts
1.82 s
Warm — script with @file:DependsOn
ktx-daemon
441 ms
ktx
686 ms
main-kts
1.98 s
Dev loop — script edited every run
ktx-daemon
684 ms
ktx
3.98 s
main-kts
4.66 s
Cold start — caches cleared
ktx-daemon
2.53 s
ktx
3.73 s
main-kts
4.23 s

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-kts1822 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

bash
# 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 bench

The 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.

Released under TBD license.