I've been running Solon apps in production for a while now, and one thing that keeps surprising people is how the server engine works.
Not "which server does Solon use" — because the answer is: you choose.
Solon doesn't bundle a hardcoded HTTP server. The framework core is about 0.3MB. The HTTP engine is a separate plugin. You pick one. Or two. Or swap them between environments without touching a single line of application code.
Here's a quick rundown of the 10 HTTP server options, what they're good for, and how to switch.
The One-Dependency Swap
In Solon, changing your HTTP server means changing one dependency in pom.xml. Nothing else.
<!-- Default: jdkhttp (built-in JDK, 0.3MB) -->
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-web</artifactId>
</dependency>
Want Undertow instead?
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.noear</groupId>
<artifactId>solon-server-jdkhttp</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-server-undertow</artifactId>
</dependency>
That's it. Your @Controller, @Mapping("/hello"), filters, interceptors — everything stays the same. The routing layer is framework-owned; the server underneath is a pluggable adapter.
The 10 Engines at a Glance
| Plugin | Size | Protocols | Reactive | JDK |
|---|---|---|---|---|
| jdkhttp | 0.3MB | http | ✅ | 8+ |
| smarthttp | 0.8MB | http, ws | ✅ | 8+ |
| grizzly | 1.8MB | http, ws, http2 | ✅ | 8+ |
| vertx | 6.3MB | http, ws, http2 | ✅ | 8+ |
| jetty | 2.7MB | http, ws | ✅ | 8+ |
| undertow | 4.6MB | http, ws, http2 | ✅ | 8+ |
| tomcat | — | http, ws, http2 | ✅ | 8+ |
| jetty-jakarta | 3.9MB | http, ws, http2 | ✅ | 17+ |
| undertow-jakarta | — | http, ws, http2 | ✅ | 17+ |
| tomcat-jakarta | — | http, ws, http2 | ✅ | 17+ |
Plus two dedicated WebSocket-only servers (solon-server-websocket at 0.4MB, solon-server-websocket-netty at 3.6MB) and one Socket.D server.
How to Choose
For microservices and edge services: jdkhttp or smarthttp
If you're building a lightweight API service or a microservice that needs to start in under a second, stick with jdkhttp. It's 0.3MB, requires zero external dependencies, and supports reactive I/O. The JDK built-in com.sun.net.httpserver is perfectly adequate for most API workloads.
smarthttp (0.8MB, Chinese-contributed) adds WebSocket support on the same port — useful if you need both REST and WebSocket without adding a second server.
@SolonMain
public class App {
public static void main(String[] args) {
Solon.start(App.class, args, app -> {
app.enableWebSocket(true);
});
}
}
When you need HTTP/2: grizzly, undertow, vertx, or tomcat
All four support HTTP/2. Enable it with a single line:
app.onEvent(HttpServerConfigure.class, e -> {
e.enableHttp2(true); // v2.3.8+
});
Undertow is a solid middle ground — 4.6MB, Apache 2.0 license, good community adoption. Vert.x is heavier (6.3MB) but gives you the full Vert.x event-loop ecosystem if you're already using it.
Traditional Java shops: Jetty or Tomcat
If your team is migrating from Spring Boot + embedded Tomcat, solon-server-tomcat is the most natural drop-in. It uses the same Tomcat v9 engine underneath — your ops team's existing tuning knowledge carries over.
Jetty is a great alternative if you prefer Eclipse ecosystem. It also has explicit JSP support via solon-server-jetty-add-jsp.
Jakarta EE migration: jetty-jakarta, undertow-jakarta, tomcat-jakarta
If you're on JDK 17+ and targeting Jakarta namespace (jakarta.*), use the Jakarta variants. They're paired with the corresponding Jakarta server versions (Jetty 12, Undertow 2.3, Tomcat 11).
The Code That Never Changes
This is the part I like most. No matter which server engine you pick, your application code looks identical:
@Controller
public class DemoController {
@Mapping("/hello")
public String hello() {
return "Hello world!";
}
@Mapping("/greet/{name}")
public String greet(String name) {
return "Hello, " + name + "!";
}
}
The same code runs on jdkhttp (0.3MB, no dependencies), Undertow (4.6MB, HTTP/2), or Tomcat (production-hardened). Zero changes.
Advanced: Multiple Ports, SSL, and Debug Mode
All server adapters share the same configuration surface:
# app.yml
server.port: 8080
server.http.port: 8080 # alias
server.ssl.keyStore: "/data/ca/demo.jks"
server.ssl.keyPassword: "demo"
Or programmatically:
Solon.start(App.class, args, app -> {
// Add a second HTTP port (useful when main port is HTTPS)
app.onEvent(HttpServerConfigure.class, e -> {
e.addHttpPort(8082);
e.enableDebug(true); // debug mode for smarthttp/jdkhttp
});
});
Custom SSLContext (v2.5.9+):
app.onEvent(HttpServerConfigure.class, e -> {
SSLContext sslContext = buildMyCustomSSLContext();
e.enableSsl(true, sslContext);
});
What About the "Default" Default?
If you add solon-web without any explicit server exclusion, Solon uses jdkhttp — the built-in JDK HTTP server. It's the lightest option (0.3MB), requires zero external JARs, and works for 90% of development scenarios.
In production, you might want something more tuned. But the beauty is: you don't decide on day one. Start with jdkhttp, benchmark, then swap to Undertow or Tomcat when you need HTTP/2 or WebSocket — all without rewriting a single route.
Honest Limitations
-
Tomcat (v9) doesn't support WebSocket directly — you need the
solon-server-tomcat-add-websocketsub-plugin. - smarthttp had a file upload size cap (~2.1GB) before v3.0.3 — fixed now, but worth noting if you're on an older version.
- Vert.x is the heaviest at 6.3MB — great for reactive workloads but overkill for a simple CRUD API.
- Jakarta variants (jetty-jakarta, undertow-jakarta, tomcat-jakarta) require JDK 17+.
Summary
Solon's pluggable server architecture is one of those features that seems small on paper but makes a big difference in practice. One dependency change, zero code impact, and a full spectrum of engines from 0.3MB to full-featured production servers.
| Need | Pick |
|---|---|
| Lightest API server | jdkhttp (0.3MB) |
| REST + WebSocket on same port | smarthttp (0.8MB) |
| HTTP/2 + solid performance | undertow (4.6MB) |
| Spring Boot migration | tomcat |
| Reactive event-loop | vert.x or grizzly |
| Jakarta EE (JDK 17+) | jetty-jakarta / undertow-jakarta |
| Standalone WebSocket | solon-server-websocket (0.4MB) |
The server is infrastructure. The framework is your code. Solon keeps them separate — and that's a good thing.
Top comments (3)
I like the separation between the framework and the transport layer. Swapping the server with a dependency change is a clean architectural boundary.
One thing I’d still encourage is benchmarking and compatibility testing before treating the engines as interchangeable. Different servers can have different threading models, timeout defaults, TLS behavior, WebSocket implementations, and performance characteristics under load. The application code may stay identical, but the operational profile often doesn’t.
That abstraction is a real strength—as long as teams validate the runtime characteristics before switching engines in production.
Thanks, Mustafa! That is exactly the design philosophy behind Solon — the framework owns the routing layer and the IoC container, while the HTTP server is just an adapter. It also makes testing easier: you can spin up the same app with jdkhttp locally and Undertow in production, same codebase, zero surprises. Glad you like it!
Thanks! I completely agree with the architectural separation. I’d just be a little careful with the phrase “zero surprises.” 😊 Zero code changes, absolutely. Zero operational differences, probably not. That’s actually one of the strengths of the design—you can benchmark the same application across different engines without changing the business logic, then choose the one whose runtime characteristics best fit your workload.