Why Tomcat Beats Jetty 15× with SpringBoot 4 Virtual Threads

A performance comparison of SpringBoot 4.0 with virtual threads shows Tomcat achieving up to fifteen times higher QPS than Jetty, while startup times remain similar, and provides step‑by‑step instructions for building native Docker images and switching containers.

macrozheng
macrozheng
macrozheng
Why Tomcat Beats Jetty 15× with SpringBoot 4 Virtual Threads

Conclusion

If you run SpringBoot 4.0 with virtual threads, choose Tomcat; Jetty shows almost no improvement.

Test Environment

SpringBoot 4.0.1

JDK 25

Machine: MacBook Pro M4, 32 GB RAM

Docker memory limit: 1024 MB

Load tool: wrk, 30 seconds per run

Test endpoint: /pay which sleeps 1 second to simulate I/O.

@Service
public class PayService {
    private static final String[] CHANNELS = new String[]{"ALIPAY", "WECHAT", "UNION", "VISA", "MASTER"};
    public String doPay(String orderId) {
        try {
            String channel = CHANNELS[ThreadLocalRandom.current().nextInt(CHANNELS.length)];
            Thread.sleep(1000); // simulate payment gateway call
            return "Order %s paid via %s".formatted(orderId, channel);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
@RestController
@RequestMapping("/pay")
public class PayController {
    private static final Logger log = LoggerFactory.getLogger(PayController.class);
    private static final AtomicInteger counter = new AtomicInteger(1);
    private final PayService payService;
    public PayController(PayService payService) { this.payService = payService; }
    @GetMapping
    public String pay(@RequestParam(required = false, defaultValue = "PIG2026") String orderId) {
        int id = counter.getAndIncrement();
        log.info("Request {} processed by {}", id, Thread.currentThread());
        String result = payService.doPay(orderId);
        log.info("Request {} resumed by {}", id, Thread.currentThread());
        return result;
    }
}

Docker Images

tomcat (no virtual threads)

tomcat‑vt (virtual threads enabled)

jetty (no virtual threads)

jetty‑vt (virtual threads enabled)

Enable virtual threads with a single property:

spring.threads.virtual.enabled=true

Native Image Build

Using GraalVM Native Image, the build commands are:

# Tomcat without virtual threads
./mvnw clean -DskipTests spring-boot:build-image -Pnative \
  -Dspring-boot.build-image.imageName=pig-pay-tomcat:4.0.0-25-native

# Tomcat with virtual threads
echo "spring.threads.virtual.enabled=true" >> src/main/resources/application.properties
./mvnw clean -DskipTests spring-boot:build-image -Pnative \
  -Dspring-boot.build-image.imageName=pig-pay-tomcat:4.0.0-25-native-vt

# Jetty without virtual threads
./mvnw clean -DskipTests spring-boot:build-image -Pjetty,native \
  -Dspring-boot.build-image.imageName=pig-pay-jetty:4.0.0-25-native

# Jetty with virtual threads
echo "spring.threads.virtual.enabled=true" >> src/main/resources/application.properties
./mvnw clean -DskipTests spring-boot:build-image -Pjetty,native \
  -Dspring-boot.build-image.imageName=pig-pay-jetty:4.0.0-25-native-vt

The resulting native images are:

pig-pay-tomcat:4.0.0-25-native

pig-pay-tomcat:4.0.0-25-native-vt

pig-pay-jetty:4.0.0-25-native

pig-pay-jetty:4.0.0-25-native-vt

Build time on an M4 Mac is roughly 3–5 minutes per image.

Benchmark Results

Baseline (no virtual threads)

Concurrency 100: Tomcat QPS 95.96, Jetty QPS 94.19 (≈ equal)

Concurrency 500: Tomcat 192.76, Jetty 187.08 (Tomcat slightly ahead)

Concurrency 1000: Tomcat 192.92, Jetty 187.06 (Tomcat slightly ahead)

Concurrency 3000: Tomcat 179.49, Jetty 171.11 (Tomcat slightly ahead)

Concurrency 5000: Tomcat 114.23, Jetty 98.66 (Tomcat ahead)

Both containers hit the 200‑thread ceiling.

With Virtual Threads

Concurrency 100: Tomcat‑VT 96.45, Jetty‑VT 95.53 (≈ equal)

Concurrency 500: Tomcat‑VT 477.99, Jetty‑VT 191.90 (Tomcat 2.5× faster)

Concurrency 1000: Tomcat‑VT 947.68, Jetty‑VT 191.96 (Tomcat 5× faster)

Concurrency 3000: Tomcat‑VT 2699.67, Jetty‑VT 178.13 (Tomcat 15× faster)

Concurrency 5000: Tomcat‑VT 616.43, Jetty‑VT 112.09 (Tomcat 5.5× faster)

Tomcat’s QPS jumps from ~179 to ~2699 (≈15×) when virtual threads are enabled, while Jetty’s QPS stays around 170‑180.

Performance chart
Performance chart

Why Jetty Lags

Jetty still respects server.jetty.threads.max=200 even with virtual threads, limiting the number of threads it can schedule. Tomcat, however, ignores server.tomcat.threads.max=200 when virtual threads are enabled, allowing many more concurrent virtual threads.

Startup Time Comparison

tomcat: 0.643 s

tomcat‑vt: 0.658 s

jetty: 0.806 s

jetty‑vt: 0.710 s

All native images start within one second, showing comparable startup performance.

Switching from Jetty to Tomcat

Replace the Jetty starter with the default spring-boot-starter-web dependency and enable virtual threads via the property above. No additional configuration is required.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
spring.threads.virtual.enabled=true

Takeaway

While SpringBoot 4.0 dropped Undertow, Tomcat remains a robust choice. With virtual threads, Tomcat can achieve dramatic throughput gains, whereas Jetty’s legacy thread limits prevent it from benefiting similarly.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

DockerspringbootVirtual Threadsnative-imageJetty
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.