Runners execute after the web server has already started. What are the readiness implications, and when should you use ApplicationReadyEvent or other alternatives instead?
answer
- web server starts during refresh, before runners
- socket open while runner still working
- ApplicationReadyEvent fires AFTER runners
- gate traffic with readiness probe, not runner
- long/optional work -> async; mandatory fail-fast -> runner
basics
~20 sThe embedded web server starts during context refresh, before runners run, so requests can arrive while a runner is still working. If startup work must gate traffic, use readiness probes plus ApplicationReadyEvent (published after runners) rather than assuming a runner blocks traffic.
solid answer
~50 sA subtle but important fact: the embedded web server (Tomcat/Netty) is started as a SmartLifecycle during context refresh, which happens before callRunners invokes the runners. So there is a window where the HTTP port is open and can accept requests while your runner is still seeding data or warming caches. Runners are synchronous on the main thread and do delay ApplicationReadyEvent, but they do not stop the server from listening. For correctness you should not treat 'runner finished' as 'safe to serve': instead rely on Spring Boot's application availability model — the readiness state flips to ACCEPTING_TRAFFIC after startup completes, and Kubernetes-style readiness probes (/actuator/health/readiness) keep traffic away until then. ApplicationReadyEvent fires after all runners complete, so an @EventListener on it is the right hook for 'everything, including runners, is done.' Also prefer an event listener or async task when startup work is long-running, optional, or must not abort the app, since a thrown runner exception kills startup.
code
java · 23 lines// Mandatory, ordered, fail-fast startup step
@Component @Order(10)
class SchemaCheck implements ApplicationRunner {
public void run(ApplicationArguments args) { /* throw to abort startup */ }
}
// "Everything (incl. runners) is up" hook — fires after all runners
@Component
class ReadyHook {
@EventListener(ApplicationReadyEvent.class)
void onReady(ApplicationReadyEvent e) { /* runners already finished here */ }
}
// Long/optional warm-up that must not block startup or kill the app
@Component
class CacheWarmer implements ApplicationRunner {
private final TaskExecutor executor;
CacheWarmer(TaskExecutor executor) { this.executor = executor; }
public void run(ApplicationArguments args) {
executor.execute(this::warm); // off the main startup thread
}
void warm() { /* ... */ }
}go deeper
Just know ApplicationReadyEvent is another startup hook and runners run at startup.
Know ApplicationReadyEvent fires after runners and that runner exceptions abort startup.
Explain the web-server-starts-before-runners timing and prefer async for long optional work.
Reason about the availability/readiness model, probe-based traffic gating, exit codes, transactions, and choosing runner vs event vs async per requirement.
## The core timing fact Inside `SpringApplication.run`, the `ApplicationContext` is **refreshed** before runners are called. Refresh includes `finishRefresh()`, which starts `SmartLifecycle` beans — and the embedded web server is wrapped in a `WebServerStartStopLifecycle` that binds and begins listening at that point. Only **after** refresh returns does Spring Boot call `callRunners(...)`. Therefore: > By the time your `CommandLineRunner`/`ApplicationRunner` runs, the HTTP port is already open and can accept connections. This breaks the naive assumption that "a runner blocks the app from serving traffic until it finishes." It blocks `ApplicationReadyEvent` and the return of `run()`, but not the listening socket. ## Why this matters If a runner spends 10 seconds seeding data, incoming requests during those 10 seconds hit an application whose data may not be ready. In a load-balanced or Kubernetes deployment this can cause errors or serve incomplete responses right after a rollout. ## The right tools for readiness Spring Boot has a first-class **application availability** model: - `LivenessState` (is the app broken?) and `ReadinessState` (should it receive traffic?). - The readiness state transitions to `ACCEPTING_TRAFFIC` only after the application is fully started (after runners and `ApplicationReadyEvent`), and Actuator exposes it at `/actuator/health/readiness`. - A Kubernetes readiness probe pointed at that endpoint keeps the load balancer from routing traffic until the app is genuinely ready — which is the correct way to gate on startup work, not the runner itself. ## Events versus runners - `ApplicationRunner`/`CommandLineRunner`: run synchronously, in order, and their exceptions **abort startup** (fail-fast). Good for mandatory, fast, ordered startup steps that should prevent the app from coming up if they fail. - `@EventListener(ApplicationReadyEvent.class)`: fires **after** all runners finish. Good for "the whole app, including runners, is up" hooks, and by default a listener exception is logged but does not tear down the context the same way (behavior differs from the fail-fast runner path). - For **optional or long-running** work (cache warm-up that shouldn't block startup), dispatch it asynchronously (e.g. `@Async` or a `TaskExecutor`) from a runner or ready-event listener so the main thread isn't held and a failure doesn't kill the app. ## Other gotchas at this level - **Transactions**: a runner's `run()` is invoked by Spring through the bean (an external call), so `@Transactional` on `run()` is honored. But long transactions during startup can hold connections — be deliberate. - **Exit codes**: for CLI-style batch apps, throwing from a runner (or implementing `ExitCodeGenerator`) lets you control the process exit code, since `SpringApplication.run` surfaces failures to `SpringApplication.exit`. - **Testing**: in `@SpringBootTest`, runner beans execute during context startup, which can slow or pollute tests; guard them with a profile/`@ConditionalOnProperty` or exclude them, or annotate with `@Profile("!test")`. - **Blocking the main thread**: because runners are synchronous, a slow runner directly extends startup time and the readiness delay. ## Summary decision guide - Mandatory, fast, must-fail-startup, ordered → runner. - "App fully ready" hook after runners → `ApplicationReadyEvent` listener. - Gate real user traffic → readiness probe / availability state, never the runner alone. - Long/optional work → async off the startup thread.
- If a runner must complete before the app serves traffic, how do you actually enforce that?Not with the runner alone — the socket is already open. Use Spring Boot's readiness state and a readiness probe (e.g. /actuator/health/readiness in Kubernetes) so the load balancer withholds traffic until startup, including runners, completes.
- When would you choose an ApplicationReadyEvent listener over a runner?When you want a hook after all runners finish, when the work is optional and shouldn't fail-fast the startup, or when you don't need ordered fail-fast semantics. Runners are for mandatory, ordered steps that should abort startup on failure.
- Does @Transactional work on a runner's run() method?Yes. Spring invokes run() through the proxied bean as an external call, so the transactional advice applies. Just be cautious about holding a transaction/connection for long startup work.
saying these in an interview costs you the question
- Assuming the web server does not accept requests until runners finish
- Using a runner as the sole mechanism to gate production traffic
- Believing ApplicationReadyEvent fires before runners
- Running long blocking work in a runner and thereby stalling startup without realizing it delays readiness