skip to content

What do the native-image optimization levels (-O0 through -O3, and -Ob) mean, and which would you use for a production throughput-bound Spring service versus local development?

level: middleimportance: should knowfreq 38%

answer

  1. -O0 debug, -O2 default, -O3 aggressive
  2. -Ob = build time, not runtime
  3. dev -> -Ob, prod -> -O3 + PGO
  4. -O3 is Oracle GraalVM
  5. higher O = slower build

basics

~20 s

-O sets compiler optimization: -O0 none (debugging), -O2 the production default, -O3 more aggressive (Oracle GraalVM), -O1 in between. -Ob optimizes build time, not runtime — use it for fast local dev builds, never for production throughput.

solid answer

~40 s

The -O build arg controls how hard native-image optimizes the generated code. -O0 disables optimizations and keeps debug info, for debugging only. -O1 is a light level. -O2 is the default and the right baseline for production. -O3 is a more aggressive level available in Oracle GraalVM and pairs well with PGO for throughput-bound services. Separately, -Ob is the 'optimize for build time' mode: it makes the build faster and cheaper at the cost of runtime performance, which is ideal for the inner dev loop where you rebuild constantly but wrong for production. So: local dev iteration uses -Ob (or -O0 when debugging); a throughput-bound production service uses -O2 as the floor and -O3 (with PGO) when you need to squeeze out peak performance. You pass it through the Native Build Tools buildArgs.

code

kotlin · 17 lines
kotlin
// build.gradle.kts — pick the optimization level per build profile
import org.graalvm.buildtools.gradle.dsl.GraalVMExtension

configure<GraalVMExtension> {
    binaries {
        named("main") {
            val level = when (project.findProperty("buildProfile")) {
                "dev"     -> "-Ob"   // fastest builds for the inner loop
                "debug"   -> "-O0"   // no opt, keep debug info
                else       -> "-O3"   // release: max runtime perf (Oracle GraalVM)
            }
            buildArgs.add(level)
        }
    }
}
// Dev:      ./gradlew nativeCompile -PbuildProfile=dev
// Release:  ./gradlew nativeCompile   (defaults to -O3)

go deeper

for a junior

Know -O2 is the default and -Ob is about build time, not runtime speed.

for a middle

List the levels and pick the right one per environment (dev -Ob, prod -O2/-O3).

for a senior

Tie -O3 to Oracle GraalVM + PGO and to CI build-time cost tradeoffs.

for a principal

Set org-wide build profiles so dev iterates fast (-Ob) while release maximizes throughput (-O3+PGO), balancing CI cost.

## The -O flag `-O<n>` is a `native-image` build argument that trades **build effort/time** against **generated-code quality (runtime performance)**. | Level | Meaning | Use it for | |-------|---------|-----------| | `-O0` | No optimizations, preserves debug info | Debugging the native image (with `-g`) | | `-O1` | Basic optimizations | Rarely chosen explicitly | | `-O2` | **Default**, balanced production optimization | Production baseline | | `-O3` | More aggressive optimization (**Oracle GraalVM**) | Throughput-bound prod, especially with PGO | | `-Ob` | Optimize **build time** (faster, cheaper builds; lower runtime perf) | Inner dev loop / fast CI feedback | **Key distinction:** `-O0..-O3` optimize the *runtime* of the produced binary (higher number = more runtime optimization, slower build). `-Ob` is different — the **'b' is for build**: it deliberately reduces optimization to make the **build itself faster**, so it *lowers* runtime performance. Confusing `-Ob` with a high runtime optimization is a classic trap. ## Choosing per environment - **Local development / fast iteration:** `-Ob` — you rebuild constantly and don't care about peak throughput; the faster build wins. Use `-O0 -g` when you specifically need to debug. - **Production, throughput-bound:** `-O2` is the floor. Move to `-O3` (Oracle GraalVM) when you need maximum steady-state performance, and combine it with **PGO** — `-O3` plus a good profile is where the biggest native throughput wins come from. - **Startup/footprint-only services:** `-O2` default is plenty; `-O3` mainly buys throughput, not startup. ## Wiring into Spring Boot Pass through the GraalVM Native Build Tools plugin: Gradle `graalvmNative { binaries { named("main") { buildArgs.add("-O3") } } }`, or Maven `native-maven-plugin` `<buildArgs><buildArg>-O3</buildArg></buildArgs>`. Teams often gate the level on a build profile so dev uses `-Ob` and the release pipeline uses `-O3`. ## Gotchas - `-O3` is an **Oracle GraalVM** level; on Community Edition stick to `-O2`. - Higher `-O` means **longer, more expensive builds** — native builds are already the slow part of CI. - `-Ob` in production silently costs you throughput; it's the most common misconfiguration. - `-O` is orthogonal to the GC choice and to PGO; you typically combine all three.

  • A teammate set -Ob for production 'to optimize'. What's wrong?
    -Ob optimizes build time, not runtime — it reduces optimization to build faster, so it lowers production throughput. Production should be -O2 (floor) or -O3, and -Ob belongs in the dev loop.
  • Where do -O levels give the most benefit?
    Combined with PGO on a throughput-bound service: -O3 plus a representative profile recovers most of the JIT gap. For startup-only workloads the default -O2 is already enough.

saying these in an interview costs you the question

  • Reading -Ob as a higher runtime optimization than -O3
  • Using -O0 or -Ob for production
  • Assuming -O3 is available on Community Edition

context