Comparing Java Application Servers: Performance, Scalability, and Deployment Strategies

Devops & Infrastructure, Java, Tips & Tricks, and Tutorials

Comparing Java Application Servers: Performance, Scalability, and Deployment Strategies

Choosing the right Java application server is one of those decisions that feels simple at first — until you're debugging a memory leak at 2 AM on a server that should have been fine. The truth is, every server makes trade-offs, and the right choice depends entirely on your deployment context: what you're running, where it's running, and how much operational overhead you're willing to accept.

This guide compares the most widely used Java application servers side by side, with honest assessments of where each one excels and where it falls short. If you're looking for a broader overview of the Java server landscape and cloud-native trends, see our companion piece on Java application servers in 2025.

The Comparison: Six Servers, Head to Head

Apache Tomcat

Tomcat is the default choice for a reason — it runs Servlet and JSP workloads with minimal configuration, and nearly every Java developer has used it at some point. It implements the Jakarta Servlet, JSP, and WebSocket specifications without the full Jakarta EE stack, which keeps it lean.

Where it shines:

  • Startup time under 3 seconds on modern hardware
  • Memory footprint of 150–250 MB for typical web applications
  • Massive ecosystem of documentation, tutorials, and community support
  • Simple server.xml configuration that most teams can understand immediately

Where it doesn't:

  • No built-in support for JMS, JPA, or CDI — you need to bring your own libraries
  • Clustering requires additional configuration with Apache mod_jk or a load balancer
  • Thread-per-request model can become a bottleneck under heavy concurrent load
  • No native support for reactive programming patterns

Best fit: Traditional web applications, REST APIs with moderate traffic, teams that want a well-understood runtime with minimal surprises.

WildFly (formerly JBoss)

WildFly is the open-source upstream of Red Hat's JBoss EAP. It provides a full Jakarta EE implementation, which means EJBs, JMS, JPA, CDI, and JAX-RS are all available out of the box. The modular classloading system (JBoss Modules) means you only load what your application actually uses.

Where it shines:

  • Full Jakarta EE 10 compliance — no need to bundle your own JPA or messaging libraries
  • Modular architecture loads only the subsystems your application needs
  • Built-in HA clustering with session replication and distributed caching via Infinispan
  • Management CLI and web console for runtime configuration without restarts
  • Domain mode for managing multiple server instances from a single point

Where it doesn't:

  • Memory footprint of 500–700 MB makes it overkill for simple applications
  • Configuration complexity is real — standalone.xml can grow to thousands of lines
  • Deployment troubleshooting often requires understanding the subsystem architecture
  • Slower adoption of non-EE standards compared to Spring ecosystem

Best fit: Enterprise applications that genuinely need Jakarta EE services (messaging, distributed transactions, managed persistence), organisations already invested in Red Hat's ecosystem.

Eclipse Jetty

Jetty is often overlooked in server comparisons because it's frequently used as an embedded server inside other frameworks (including Spring Boot itself). But Jetty as a standalone server is remarkably capable, particularly for HTTP/2 and WebSocket workloads.

Where it shines:

  • Extremely low memory footprint — 50–150 MB for typical deployments
  • First-class HTTP/2 and WebSocket support with efficient connection handling
  • Embeddable design means you can programmatically configure everything in Java
  • Non-blocking I/O architecture handles high concurrent connections efficiently
  • Excellent for long-lived connections (WebSocket, SSE) where thread-per-request models struggle

Where it doesn't:

  • Smaller community than Tomcat — fewer Stack Overflow answers and tutorials
  • No built-in Jakarta EE services beyond Servlet/JSP
  • Admin tooling is basic compared to WildFly's management console
  • Configuration is less intuitive for teams used to XML-based server configuration

Best fit: Applications with high WebSocket or HTTP/2 traffic, embedded server use cases, microservices that need minimal resource consumption.

Open Liberty (IBM)

Open Liberty is IBM's open-source application server, the modern successor to WebSphere Liberty and traditional WebSphere. It uses a feature model where you declare exactly which Jakarta EE specifications you need, and the server loads only those.

Where it shines:

  • Zero-migration architecture — applications written for Java EE 7 run alongside Jakarta EE 10 applications
  • Feature-based composition means you pay (in memory) only for what you use
  • MicroProfile support for cloud-native patterns (health checks, metrics, fault tolerance, config)
  • InstantOn technology with CRaC (Coordinated Restore at Checkpoint) for sub-second startup
  • Enterprise-grade monitoring integration with OpenTelemetry and Prometheus

Where it doesn't:

  • Smaller community than Tomcat or Spring Boot — hiring developers with Liberty experience is harder
  • Some advanced features require IBM Semeru Runtime (IBM's JDK) for best performance
  • Documentation can lag behind the rapidly evolving feature set
  • Enterprise support requires a commercial WebSphere Liberty licence

Best fit: Enterprises migrating from traditional WebSphere, applications that need both Jakarta EE compliance and cloud-native capabilities, regulated industries where IBM support contracts matter.

Quarkus

Quarkus was built specifically for containers and Kubernetes. It moves as much work as possible from runtime to build time — dependency injection resolution, configuration parsing, and even some reflection-based operations are handled during compilation rather than at startup.

Where it shines:

  • Native compilation via GraalVM produces binaries that start in under 50 milliseconds
  • JVM mode still starts in 1–2 seconds with 70–80 MB memory footprint
  • Developer experience with live reload (quarkus dev) rivals interpreted languages
  • Reactive and imperative programming models in the same application
  • Built-in support for MicroProfile, Hibernate, RESTEasy, and a growing extension ecosystem

Where it doesn't:

  • GraalVM native compilation is slow (minutes, not seconds) and can fail on reflection-heavy libraries
  • Not all Java libraries work in native mode — you may hit compatibility walls
  • Relatively young ecosystem compared to Spring — some extensions are less mature
  • Opinionated framework that may conflict with existing codebases

Best fit: Cloud-native microservices, serverless functions, applications where startup time and memory density directly affect cost (Kubernetes pod scaling, AWS Lambda).

Spring Boot (Embedded Server)

Spring Boot isn't an application server — it embeds one (Tomcat, Jetty, or Undertow) inside your application. The distinction matters because with Spring Boot, the application is the deployment artifact. There's no deploying to a server step; you deploy the fat JAR directly.

Where it shines:

  • Largest ecosystem and community in the Java world — if it exists, there's a Spring starter for it
  • Auto-configuration eliminates boilerplate for common patterns (data access, security, messaging)
  • Spring WebFlux provides a mature reactive stack for high-concurrency scenarios
  • Embedded server means no separate server installation, configuration, or management
  • Excellent integration testing support with @SpringBootTest

Where it doesn't:

  • Fat JARs can be large (50–100+ MB) with all dependencies bundled
  • Startup time of 5–15 seconds for non-trivial applications (mitigated by Spring Boot 3.x AOT)
  • Auto-configuration magic can make debugging surprising behaviour difficult
  • Memory consumption of 200–500 MB for typical applications is higher than Quarkus native

Best fit: Most Java applications, honestly. The ecosystem breadth makes it the default choice unless you have a specific reason to choose something else (Jakarta EE compliance, native compilation, ultra-low memory).

Performance Comparison at a Glance

The numbers below reflect typical production workloads (REST API serving JSON, moderate database access). Real performance depends heavily on your application code, JVM tuning, and infrastructure.

Server Cold Start (JVM) Memory (Typical) Throughput (REST API) Best Concurrency Model
Tomcat 10.1 2–4s 150–250 MB 2,500–4,000 req/s Thread-per-request
WildFly 31 4–8s 500–700 MB 3,500–5,000 req/s Thread-per-request + async
Jetty 12 1–3s 50–150 MB 3,000–4,500 req/s Non-blocking I/O
Open Liberty 24 3–6s 200–400 MB 3,500–5,000 req/s Thread-per-request + reactive
Quarkus 3.x (JVM) 1–2s 70–130 MB 3,500–5,000 req/s Reactive + imperative
Quarkus 3.x (Native) 0.02–0.05s 30–70 MB 3,000–4,500 req/s Reactive + imperative
Spring Boot 3.x 5–15s 200–500 MB 3,000–4,500 req/s Thread-per-request + WebFlux

A note on benchmarks: These ranges come from publicly available TechEmpower benchmarks and Red Hat/IBM published comparisons. Your mileage will vary significantly based on application complexity, database latency, and JVM tuning. Don't choose a server based on throughput numbers alone — operational characteristics (clustering, management, ecosystem) matter more in practice.

The Jakarta EE Migration Factor

If your codebase still uses javax.* packages, you'll need to migrate to jakarta.* for any server running Jakarta EE 10+. This affects Tomcat 10.1+, WildFly 27+, and Open Liberty with Jakarta EE 10 features enabled.

The migration itself is usually mechanical — tools like the Eclipse Transformer can automate the namespace change. But it can surface hidden issues in third-party libraries that haven't been updated. Factor this into your server selection timeline.

Spring Boot 3.x requires Jakarta EE 9+ and handles the namespace migration internally through its starter dependencies. Quarkus uses Jakarta namespaces natively.

Deployment Strategies: WAR vs Fat JAR vs Container Image

The deployment model you choose constrains your server options:

WAR Deployment (Traditional)

Deploy a .war file to a running server instance. The server manages the application lifecycle.

# Build the WAR
mvn clean package

# Deploy to Tomcat via manager
curl -u admin:password -T target/app.war \
  "http://localhost:8080/manager/text/deploy?path=/app&update=true"

# Deploy to WildFly via CLI
$JBOSS_HOME/bin/jboss-cli.sh --connect \
  --command="deploy target/app.war --force"

Pros: Multiple applications per server instance, hot deployment, centralised server configuration. Cons: Server version management, shared classloader issues, works on my machine deployment gaps.

Fat JAR Deployment (Modern)

The application bundles its own server. You deploy and run the JAR directly.

# Spring Boot
mvn clean package
java -jar target/app.jar --server.port=8080

# Quarkus
mvn clean package -Dquarkus.package.jar.type=uber-jar
java -jar target/app-runner.jar

Pros: Self-contained, reproducible, same artifact from dev to production. Cons: Larger artifacts, no shared server management, JVM tuning per application.

Container Image (Cloud-Native)

Package the application (with or without a JVM) as a Docker container image.

# Multi-stage build for Spring Boot
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY . .
RUN ./mvnw clean package -DskipTests

FROM eclipse-temurin:21-jre
COPY --from=build /app/target/app.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
# Quarkus native build
FROM quay.io/quarkus/ubi-quarkus-mandrel-builder-image:jdk-21 AS build
COPY . /app
WORKDIR /app
RUN ./mvnw package -Dnative

FROM quay.io/quarkus/quarkus-micro-image:2.0
COPY --from=build /app/target/*-runner /application
ENTRYPOINT ["./application"]

Pros: Immutable deployments, consistent environments, orchestration via Kubernetes. Cons: Image build time, container registry management, debugging inside containers.

Build Tool Integration: Maven and Gradle

Both Maven and Gradle work with all servers listed above. The choice between them is increasingly a team preference rather than a technical constraint.

Maven remains the most common build tool in enterprise Java. Its convention-over-configuration approach and the pom.xml format are well understood. Server-specific plugins handle deployment:

<!-- WildFly deployment plugin -->
<plugin>
  <groupId>org.wildfly.plugins</groupId>
  <artifactId>wildfly-maven-plugin</artifactId>
  <version>5.0.1.Final</version>
  <configuration>
    <hostname>localhost</hostname>
    <port>9990</port>
  </configuration>
</plugin>

Gradle is gaining adoption, especially in teams already using it for Android development or multi-module builds. Its incremental build performance is notably faster for large projects:

// build.gradle
plugins {
    id 'org.springframework.boot' version '3.3.0'
    id 'io.spring.dependency-management' version '1.1.5'
}

tasks.named('bootJar') {
    archiveFileName = 'app.jar'
}

Deploying Java Applications with DeployHQ

DeployHQ supports Java deployments with OpenJDK (8, 11, 17, and 21) and Maven available in the build pipeline. A typical Java deployment workflow looks like this:

flowchart LR
    A[Git Push] --> B[DeployHQ Build]
    B --> C[mvn package]
    C --> D[Deploy JAR/WAR]
    D --> E[Run Post-Deploy]
    E --> F[Health Check]

Build commands in DeployHQ run your Maven or Gradle build, producing the deployment artifact. Post-deploy commands handle the application restart — whether that's restarting a systemd service, triggering a zero-downtime deployment, or updating a Docker container.

For teams running multiple Java services, DeployHQ's multi-server deployment and environment management features let you deploy the same artifact to staging and production with environment-specific configuration.

Decision Framework

Skip the feature matrices — here's how to actually decide:

  1. Do you need full Jakarta EE? If you need EJBs, JMS, or distributed transactions, your choices are WildFly or Open Liberty. If not, don't pay the complexity cost.

  2. Is startup time critical? For serverless, auto-scaling Kubernetes, or edge deployments, Quarkus native or Jetty will serve you better than WildFly or traditional Spring Boot.

  3. How large is your team? Spring Boot's ecosystem and hiring pool is unmatched. Choosing a niche server means accepting a smaller talent pool and fewer Stack Overflow answers.

  4. Are you migrating from WebSphere? Open Liberty provides the smoothest migration path with its zero-migration architecture.

  5. Do you need WebSocket or HTTP/2 performance? Jetty excels here. Tomcat can do it, but Jetty's non-blocking I/O architecture handles long-lived connections more efficiently.

  6. What does your ops team support? The best server is the one your team can operate, monitor, and troubleshoot at 3 AM. A Kubernetes-native Quarkus deployment is worthless if your ops team only knows Tomcat.


Ready to streamline your Java deployments? DeployHQ integrates with your Git repository and handles the build-deploy-restart cycle automatically — whether you're deploying Tomcat WARs, Spring Boot JARs, or Docker containers. Start deploying for free.

If you have questions or need help setting up your Java deployment pipeline, reach out at support@deployhq.com or find us on Twitter/X.