Files
spring-boot-demo/spring-boot-startup-time/docs/02-turning-instrumentation-on.md
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.1 KiB

02 — Turning instrumentation on

← prev 01 — The number Boot logs · next → 03 — The four phases


There is no property for this

The single most common wasted hour: looking for spring.application.startup=buffering in application.yaml. It does not exist. ApplicationStartup has to be set on the SpringApplication before run(), because the steps you care about are recorded before any configuration file has been read.

public static void main(String[] args) {
    SpringApplication app = new SpringApplication(StartupDiagnosisApplication.class);
    int capacity = Integer.getInteger("startup.buffer", 16384);
    switch (System.getProperty("startup.tracking", "buffering")) {
        case "buffering" -> app.setApplicationStartup(new BufferingApplicationStartup(capacity));
        case "jfr" -> app.setApplicationStartup(new FlightRecorderApplicationStartup());
        default -> { }
    }
    app.run(args);
}

That means changing which tracker you use is a redeploy, not a config change — worth knowing before an incident, not during one. See StartupDiagnosisApplication.java.

Exposing the endpoint

startup is not web-exposed by default:

management:
  endpoints:
    web:
      exposure:
        include: health,info,startup,beans,conditions

Exposing it without installing a tracker gives you an endpoint that returns an empty timeline rather than an error, which is a confusing way to lose twenty minutes.

GET peeks, POST drains

This is the trap that costs people their only recording. From docs/output/02-startup-tree.txt:

  GET   /actuator/startup -> events in response: 400
  POST  /actuator/startup -> events in response: 400
  POST  /actuator/startup -> events in response: 0
  GET   /actuator/startup -> events in response: 0

POST calls drainBufferedTimeline(), which empties the buffer so the memory can be reclaimed. Every guide shows the POST. If you pipe it to a file and the file is wrong, the data is gone — the application has to be restarted to record it again. Use GET while you are still working out what you want.

You do not need a @Bean for it

Boot registers the instance as a singleton named applicationStartup before the context refreshes, so it can simply be injected. Declaring your own @Bean produces:

Parameter 0 of constructor in ...StartupDiagnosticsEndpoint required a single bean,
but 2 were found:
	- bufferingApplicationStartup: defined by method 'bufferingApplicationStartup' ...
	- applicationStartup: a programmatically registered singleton

Inject the ApplicationStartup interface and narrow with instanceof, not the concrete BufferingApplicationStartup — otherwise the application refuses to start whenever somebody runs it without tracking. Full transcript in docs/output/01-api-corrections.txt.


← prev 01 — The number Boot logs · next → 03 — The four phases