Files
Ankur Mhatre 958b401f0f Spring Boot startup time: bean-by-bean diagnosis, and one directory per post
Adds spring-boot-startup-time/, the companion project for BLOG-618: a runnable
Spring Boot 4.1.1 application on JDK 25 that installs BufferingApplicationStartup
and FlightRecorderApplicationStartup behind a system property, and a /diag/startup
endpoint that computes step self time -- the number /actuator/startup does not give
you and the one that names the actual culprits.

Captured under docs/output/: the step tree sorted both ways, the same startup as JFR
events, a +5000-class experiment putting 0.11 ms per scanned class on the classpath
scan tax, the silent truncation a 2048-step buffer performs, and JDK 25 AOT cache
timings (6.93 s to 4.82 s). Post body and metadata live in post/.

Moves the existing Actuator project into actuator-in-production/ so the repository
holds one directory per article; the root README is now an index.
2026-09-05 00:17:37 +05:30

3.7 KiB

07 — Failure modes

← prev 06 — The classpath-scan tax · next → 08 — What actually helps


The buffer truncates silently

BufferingApplicationStartup takes a capacity in its only constructor. Every guide picks 2048. Nothing tells you what happens when the application produces more steps than that — so here it is, with 5,000 extra @Component classes on the classpath (docs/output/06-buffer-overflow.txt):

--- capacity 2048 ---
  started, recorded steps: 2048
--- capacity 16384 ---
  started, recorded steps: 5400

The endpoint returns 200 with a well-formed answer. grep -ic buffer app.log returns 0. There is no warning and no exception.

It is worse than losing a suffix. Steps are recorded when they end, so the buffer keeps the first ones to finish — the innermost leaves — and drops the enclosing ones:

  first 3: ['spring.boot.application.starting',
            'spring.boot.application.environment-prepared',
            'spring.boot.application.context-prepared']
  last 3 : ['spring.beans.instantiate', ...]
  last tags: [{'key': 'beanName', 'value': 'bulk2718'}]

spring.context.refresh, spring.boot.application.started and spring.boot.application.ready are absent from the truncated timeline. Any self-time calculation is now missing its outer frame, and any "the whole refresh took N" figure is simply not in the data.

The capacity is the number of steps, not beans, and this application records slightly more than one step per bean. 16384 costs a few megabytes that are freed on the first drain. Pick the large number.

aFullBufferTruncatesSilentlyAndLosesTheOuterSteps in StartupTimelineContractTests pins this behaviour.

POST destroys the recording

Covered in chapter 02, repeated here because it is the most expensive one: POST /actuator/startup drains. The second call returns an empty timeline, and so does every subsequent GET. If you are following a guide that pipes the POST into jq and the jq expression is wrong, restart the application — the data is gone.

Injecting the concrete type

public StartupDiagnosticsEndpoint(BufferingApplicationStartup startup) { ... }

compiles, works in every test, and stops the application from starting the first time somebody runs it without -Dstartup.tracking=buffering, because the registered singleton is then a DefaultApplicationStartup. Inject the interface, narrow with instanceof, and answer honestly when tracking is off.

Killing the app by its main class

Not a Spring problem, but it cost a run while producing this repository. pkill -f StartupDiagnosisApplication also matches the shell that is running the script that contains that string, and kills it. scripts/stop.sh matches on the executable being java and the jar name:

ps -eo pid=,comm=,args= | awk '$2 == "java" && /startup-diagnosis-1\.0\.0\.jar/ { print $1 }'

Trusting a single run

Startup timings on a shared or containerised machine move by several hundred milliseconds between runs of the same jar — see the per-run columns in docs/output/05-what-helps.txt, where the same configuration produced 6.456 s and 7.473 s. Take a median of four or more, and treat any difference under about 10% as noise. Every number in this repository that matters is a ratio between two such medians, not a single measurement.


← prev 06 — The classpath-scan tax · next → 08 — What actually helps