Why Spring Boot 4.0.1’s Virtual Thread Bug Could Kill Your Performance—and How to Fix It
Spring Boot 4.0.1 introduced a critical virtual‑thread configuration bug that disables Jetty’s virtual threads, causing massive CPU waste and slow responses; the article explains the bug’s cause, the official fix in v4.0.1, and provides upgrade steps, workarounds, and monitoring tips.
Spring Boot 4.0 Evolution
Spring Boot 4.0.0 was released in November 2025, built on Spring Framework 7 with modular architecture, JSpecify null‑safety, Java 25 support, and virtual‑thread features. The follow‑up patch 4.0.1 arrived in December 2025, fixing 25 bugs and updating many dependencies.
Virtual Thread Configuration Failure
Bug Symptoms
When spring.threads.virtual.enabled=true is set, Jetty should automatically use virtual threads, promising up to 50× higher concurrency. In practice Jetty continued to use traditional platform threads, leading to:
CPU resources idle (only ~20% utilization while queues grow)
Response time remains around 500 ms instead of the expected 150 ms
Projected cost savings from virtual threads never materialize
Root Cause
The issue stems from the modular refactor of Spring Boot 4.0, where the automatic configuration for Jetty’s virtual‑thread support was misplaced in an else branch and the property binding occurs after Jetty initialization.
Modular architecture split the monolithic JAR into fine‑grained modules; the virtual‑thread initialization logic was incorrectly placed.
In JettyWebServerFactoryCustomizer, the Executor configuration depends on spring.threads.virtual.enabled, but the property is bound too late, so Jetty ignores it.
Lack of integration tests meant the bug only appeared under real high‑concurrency pressure, not in unit tests.
Official Fix in Spring Boot 4.0.1
The Spring Boot team fixed the problem by reordering the initialization sequence and strengthening the conditional checks.
Core Fix Code (pseudo‑code)
// JettyServletWebServerFactory.java – after fix
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
JettyWebServer webServer = new JettyWebServer(prepareServer(), this.resourceLoader, isVirtualThreadsEnabled());
if (isVirtualThreadsEnabled()) {
// Inject virtual‑thread executor before server starts
webServer.setExecutor(createVirtualThreadExecutor());
}
return webServer;
}
private boolean isVirtualThreadsEnabled() {
return this.threadProperties != null &&
this.threadProperties.getVirtual() != null &&
this.threadProperties.getVirtual().isEnabled();
}Fix Strategy Explained
Early property collection – bind spring.threads.virtual.enabled before Jetty creation.
Explicit executor injection – call setExecutor() directly instead of relying on Jetty’s discovery.
Add integration tests – include Gatling load‑test scenarios with 1000+ concurrent requests to verify virtual threads are active.
Developer Recommendations
Upgrade Advice
If you are using any Spring Boot 4.x version, upgrade to 4.0.1 immediately.
# Maven project
./mvnw versions:update-properties -Dincludes=org.springframework.boot:*
# Gradle project
./gradlew dependencyUpdatesVerification Test
@SpringBootTest
class VirtualThreadVerificationTest {
@Autowired
private WebServer webServer;
@Test
void virtualThreadsShouldBeEnabled() {
if (webServer instanceof JettyWebServer) {
Executor executor = ((JettyWebServer) webServer).getServer().getThreadPool();
assertThat(executor).isInstanceOf(VirtualThreadExecutor.class);
}
}
}Temporary Workaround
When upgrading is not possible, override the Jetty factory to force virtual‑thread executor injection.
@Configuration
public class VirtualThreadWorkaround {
@Bean
public JettyServletWebServerFactory jettyFactory(ThreadProperties threadProperties) {
return new JettyServletWebServerFactory() {
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
JettyWebServer server = super.getWebServer(initializers);
if (threadProperties.getVirtual().isEnabled()) {
server.getServer().setExecutor(createVirtualThreadExecutor());
}
return server;
}
private Executor createVirtualThreadExecutor() {
return Executors.newVirtualThreadPerTaskExecutor();
}
};
}
}Pitfalls and Lessons from Major Version Upgrades
Virtual threads are not a silver bullet; the runtime and framework must correctly support them.
Modularization reduces JAR size but increases the risk of missing configuration – full‑stack load testing is essential.
Choose between reactive programming and virtual threads based on workload characteristics.
Observability matters – lack of thread‑pool monitoring delayed detection of this bug.
Monitoring Configuration
Enable actuator endpoints to observe thread usage in production or pre‑release environments.
management:
endpoints:
web:
exposure:
include: threads,metrics
endpoint:
threads:
enabled: trueAccess /actuator/threads to see real‑time distribution of thread types.
Conclusion
Spring Boot 4.0.1’s virtual‑thread configuration fix is a highlight of the first patch release for the 4.x line. It restores the expected 50× concurrency boost on Jetty and reduces response times by about 70 %. The release also marks the end of maintenance for the 3.2‑3.5 series, urging users to migrate to 4.x promptly.
References
Spring Boot 4.0.1 Release Notes:
https://github.com/spring-projects/spring-boot/releases/tag/v4.0.1Spring Boot 4.0 Migration Guide:
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-GuideSpring Boot project page:
https://spring.io/projects/spring-bootJava Tech Enthusiast
Sharing computer programming language knowledge, focusing on Java fundamentals, data structures, related tools, Spring Cloud, IntelliJ IDEA... Book giveaways, red‑packet rewards and other perks await!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
