Java’s HttpServer for fun and profit.

Just a decorative image for the page.

Intro

There’s a persistent belief in the Java community that com.sun.net.httpserver.HttpServer is some kind of “internal JDK thing” you shouldn’t touch.

That belief is wrong.

Yes, the package name starts with com.sun.*, which looks like it belongs in the “hands off” pile—but com.sun.net.httpserver.HttpServer is not the same category as sun.* internals. It’s part of any certified JRE, it’s documented, and it’s not stamped with “experimental” or “not for production use”. In other words: we can just use it. Cool!

That raises a practical question:

Do we always need Jetty, Netty, Undertow, etc. just to serve HTTP? And if we replace a third-party HTTP server with the one that comes with the JDK, what do we gain (and lose)?

One obvious benefit is the boring-but-important one: fewer dependencies. Fewer dependencies generally means:

  • smaller binaries/images,
  • simpler upgrades and fewer CVE fire drills
  • a smaller attack surface (especially around supply chain risk),
  • less code you didn’t write and don’t have to review and understand

And there’s another fun side effect here: if you build a tiny framework on top of the JDK’s HttpServer, you can end up with an almost absurdly small “framework + HTTP server” footprint. In the case of NinjaX, the minimal setup comes out at roughly ~100 KB (~0.1 MB)—which is very likely among the smallest (and arguably the smallest) Java “real web framework” stacks that can actually serve HTTP.

Some time ago I did something stupid but fun and rewrote Ninja—a Java web framework from the 2010s—into modern Java using AI and the latest goodies of the Java world, and called it NinjaX.

And because it was so much fun, I had another idea: why not replace Jetty (the current default web server in NinjaX) with the JDK’s built-in HttpServer? This post is about exactly that, complete with performance tests and links showing what it looks like.

Using AI to do the conversion

The fun part: I already had a working Jetty-based implementation. The annoying part: Jetty gives you a lot of things “for free” (request parsing, cookies, multipart form handling, etc.), while Java’s HttpServer is intentionally minimal.

That’s exactly where AI helped.

I used ChatBox (not a full coding agent), and a small back-and-forth centered around my NinjaJetty class. The prompt was essentially:

“Can you rewrite this file to not use Jetty, but only use com.sun.net.httpserver.HttpServer; (with executor server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());)”

And… it worked surprisingly well. After some back and forth with the AI it even handled the most annoying part: multipart parsing (including newline edge cases).

The only manual hint I gave the AI was to add a more performant executor:

httpServer.setExecutor(Executors.newVirtualThreadPerTaskExecutor());

A few notes from this exercise:

  • Java’s HttpServer is very minimal by design. If you want cookie parsing, multipart parsing, and other HTTP conveniences, you implement them yourself (or you let the AI draft them and then you test/verify).
  • I later added tests - highly recommended, because request parsing bugs are the kind that look fine until they ruin your day in production.
  • Without AI, I doubt I would have finished as quickly. Especially multipart parsing: I’d estimate 1–2 days of work for a careful, correct implementation.

Benchmarking results (not scientific, but still useful)

Let’s be honest: microbenchmarks are easy to lie with—often unintentionally. This is not a production-like benchmark.

But it can still answer a coarse question: Is Java’s built-in HttpServer in the same performance ballpark as Jetty for a trivial endpoint?.

To that end I’ve created a small repository with all the code needed for testing.

Setup

  • Machine: Mac M1 Max, 32 GB RAM
  • Tool: bombardier
  • Command:
bombardier -c 100 -n 5000000 http://localhost:8080/health
  • Endpoint: /health returns a simple string (NinjaX, no JSON, no DB, no real payload)

HttpServer (baseline - with default executor)

This was supposed to be slow, as the default executor will run everything in one thread. But it’s useful to establish a baseline and see whether other executors improve the performance.

Dependency:

<dependency>
  <groupId>org.r10r</groupId>
  <artifactId>ninjax-core</artifactId>
  <version>${ninja.version}</version>
</dependency>

Results:

Statistics        Avg      Stdev        Max
  Reqs/sec     47132.27    4668.50   61810.37
  Latency        2.12ms     1.07ms   305.37ms
  HTTP codes:
    1xx - 0, 2xx - 5000000, 3xx - 0, 4xx - 0, 5xx - 0
    others - 0
  Throughput:     8.85MB/s

Binary size: ~0.1 MB (~100 KB) — i.e., framework + HTTP server with no third-party server dependency (and mostly just NinjaX, no extra JSON/db/etc).

HttpServer (with newVirtualThreadPerTaskExecutor)

Same dependency as above, but with:

httpServer.setExecutor(Executors.newVirtualThreadPerTaskExecutor());

Results:

Statistics        Avg      Stdev        Max
  Reqs/sec     82252.91   14664.57  152264.38
  Latency        1.21ms   469.38us   127.44ms
  HTTP codes:
    1xx - 0, 2xx - 5000000, 3xx - 0, 4xx - 0, 5xx - 0
    others - 0
  Throughput:    15.45MB/s

Binary size: still ~0.1 MB (~100 KB) — again, framework + HTTP server only.

Twice as fast with only one line changed as the default executor. Nice!

Jetty

Dependency:

<dependency>
  <groupId>org.r10r</groupId>
  <artifactId>ninjax-jetty</artifactId>
  <version>${ninja.version}</version>
</dependency>

Results:

Statistics        Avg      Stdev        Max
  Reqs/sec     79878.89    9117.21  129107.62
  Latency        1.25ms     1.47ms   362.39ms
  HTTP codes:
    1xx - 0, 2xx - 5000000, 3xx - 0, 4xx - 0, 5xx - 0
    others - 0
  Throughput:    15.34MB/s

Binary size note: ~3.3 MB (primarily the Jetty stack).

Interesting - it’s even a bit slower than the HttpServer with a proper executor.

Discussion: what this does (and doesn’t) prove

This benchmark is intentionally simple, which is also exactly why it’s not representative:

  • The payload and response are tiny.
  • There’s no JSON serialization, no templates, no DB calls, no TLS, no proxy headers, no auth, no real routing complexity.
  • Jetty wasn’t tuned or “pushed”; it even went through servlet mapping, which isn’t the leanest path.
  • newVirtualThreadPerTaskExecutor() may or may not be ideal for HttpServer in every workload. (Virtual threads shine when you block; they’re not magic pixie dust for every CPU-bound case.)

And about the size difference: yes, switching from Jetty to pure JDK reduced the footprint (7.1 MB → 3.9 MB). But it’s easy to over-interpret that number. Real services usually pull in DB drivers, observability, config, auth, JSON, etc. Once you add those, the relative percentage shrinks.

Still, the “tiny baseline” is real: NinjaX + JDK HttpServer comes out to ~100 KB (~0.1 MB) for “framework + HTTP server”. That’s small enough that it’s very likely at (or near) the smallest end of the Java ecosystem for something that can actually accept HTTP requests, route them, and return responses—without pulling in a full third-party server stack.

Still, a few useful takeaways emerge:

  • Performance is in the same ballpark. For a trivial endpoint, JDK HttpServer plus virtual threads can match (or slightly beat) a basic Jetty setup in raw req/s.
  • You can meaningfully reduce dependencies if your needs are simple and you’re willing to implement a few missing HTTP conveniences.
  • The baseline footprint can be tiny. ~100 KB (~0.1 MB) for “framework + HTTP server” is not just small—it’s borderline ridiculous by Java standards.
  • For certain classes of services—internal tools, lightweight APIs, embedded admin endpoints, sidecars, test fixtures—this can be a very attractive trade.

Summary

If you want a small, dependency-light Java HTTP server, don’t overlook what the JDK already ships.

  • com.sun.net.httpserver.HttpServer is minimal, but usable.
  • With a virtual-thread executor, it can deliver throughput comparable to Jetty in simple scenarios.
  • Size/footprint: with the JDK HttpServer, NinjaX lands at roughly ~100 KB (~0.1 MB) for “framework + HTTP server” — very likely among the smallest Java web stacks that can actually serve HTTP.
  • The big trade-off is convenience: you’ll need to implement (or pull in) things Jetty normally provides—cookies, multipart parsing, and other HTTP ergonomics.
  • AI is a surprisingly effective accelerator for this kind of “porting work”, especially when you already have a working reference implementation and you validate with tests.

If you’re optimizing for fewer dependencies and a smaller supply-chain footprint, Java’s built-in HttpServer can be a practical option—not just a curiosity.

More

Related posts