This demo sends ten concurrent requests to a blocking Spring MVC endpoint. Tomcat is deliberately limited to one platform worker, so the batch takes roughly ten seconds without virtual threads. With one Spring Boot property, the same batch finishes in roughly one second because each waiting request can use a virtual thread.
Prerequisites: Java 25 and hey (brew install hey).
-
Run the tests:
./mvnw test -
Start the application:
./mvnw spring-boot:run
-
Send ten concurrent requests from another terminal:
hey -n 10 -c 10 http://localhost:8080/benchmark | awk '/Total:/ {print $2 "s"}'
With virtual threads enabled, the elapsed time should be about one second.
1.0069s
First remove or comment out this line in application.properties:
spring.threads.virtual.enabled=trueRestart the app and run the hey command. The single Tomcat platform worker processes ten one-second requests sequentially, so the elapsed time is about ten seconds.
Restore the property, restart, and run the identical command again. Spring Boot configures Tomcat with virtual threads, allowing all ten blocking requests to wait concurrently and complete in about one second.
ONE PLATFORM THREAD ████████████████████ 10.05 s
VIRTUAL THREADS ██ 1.01 s
Measured on Java 25 with Spring Boot 4.1, three runs per configuration.
@GetMapping("/benchmark")
public BenchmarkResult benchmark() throws InterruptedException {
Thread.sleep(1_000); // Simulates waiting on blocking I/O.
Thread thread = Thread.currentThread();
return new BenchmarkResult(
"Blocking work complete",
1_000,
thread.getName(),
thread.isVirtual());
}The configuration deliberately constrains the platform-thread baseline:
server.tomcat.threads.max=1
server.tomcat.threads.min-spare=1
spring.threads.virtual.enabled=trueThis is an educational blocking-I/O demonstration, not a general-purpose microbenchmark. Virtual threads improve concurrency for waiting work; they do not make CPU-bound work faster or remove downstream capacity limits.
Watch the companion YouTube Short.