← SuperJ Manual

Building the fastest web server in the world

A step-by-step tutorial. You'll write a complete HTTP server on SuperJ's built-in HTTP stack, compile it to a single native binary, run that binary directly, and load-test it with wrk. No framework to install, no runtime to ship — the end product is one executable you can copy anywhere and run.

The finished program ships with SuperJ at demo/sj/demo/WebServer.sj (sj.demo.WebServer) — you can compile that directly (Step 4) or build it up here.

Prerequisites: SuperJ installed (superj --help works, $SJ_HOME set — see Getting Started) and wrk for the benchmark step.


Step 1 — Create the file

SuperJ follows Java's package convention: package sj.demo lives in a sj/demo/ directory. Create sj/demo/WebServer.sj, and start with the package line and the imports we'll need:

package sj.demo;

import sj.http.HttpRequest;
import sj.http.HttpRequestHandler;
import sj.http.HttpSessionProvider;
import sj.lang.Integer;
import sj.net.NioAdapter;
import sj.net.NioConfig;
import sj.net.SessionWriter;
import sj.util.ByteArray;

These come from the SDK — sj.http (HTTP parsing/serving), sj.net (the non-blocking socket adapter), and sj.util.ByteArray (a reusable byte buffer).

Step 2 — Write the request handler

A handler implements HttpRequestHandler.handle(request, writer) — it's called once per HTTP request. We build the 200 OK response once in the constructor and reuse it, so serving a request allocates nothing:

class HelloHandler implements HttpRequestHandler {
    private final ByteArray response;

    HelloHandler() {
        string body = "Hello from SuperJ!\n";
        this.response = new ByteArray(
            "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: "
            + body.length() + "\r\n\r\n" + body);
    }

    public void handle(HttpRequest request, SessionWriter writer) {
        this.response.rewind();       // reuse the pre-built buffer
        writer.write(this.response);
    }
}

Step 3 — Start the server

WebServer.main opens a listen socket and runs an event loop. HttpSessionProvider wires incoming connections through HTTP parsing to your handler; adapter.poll(0) drives the sockets on a single thread:

public class WebServer {
    public static void main(String[] args) {
        int port = 8080;
        if (args.length > 0) { port = Integer.parseInt(args[0]); }

        NioConfig cfg = new NioConfig(65536, 16384, 64, 262144, true, 1024);
        NioAdapter adapter = new NioAdapter(cfg);
        adapter.open("127.0.0.1", port, new HttpSessionProvider(new HelloHandler()));

        System.out.println("SuperJ web server listening on http://127.0.0.1:" + port + "/");
        while (true) { adapter.poll(0); }   // one event loop, one thread
    }
}

That's the whole server: a package line, one handler, one main.

Step 4 — Compile it to a native binary

superj compile sj/demo/WebServer.sj --sdk-path "$SJ_HOME/sdk" --link --output webserver

--link produces a finished executable, not just object code. The result is webserver: a single, self-contained native binary — no JVM, no interpreter, no runtime to install alongside it. Copy it to another machine of the same OS/arch and it just runs.

Don't want to type it out? The same program ships with SuperJ — compile the bundled copy: superj compile "$SJ_HOME/demo/sj/demo/WebServer.sj" --sdk-path "$SJ_HOME/sdk" --link --output webserver.

Step 5 — Run the binary

./webserver 8080
SuperJ web server listening on http://127.0.0.1:8080/

You're running the executable directly — nothing else is involved.

Step 6 — Check it

In another terminal:

curl http://127.0.0.1:8080/
Hello from SuperJ!

Step 7 — Benchmark it with wrk

wrk -t2 -c100 -d30s --latency http://127.0.0.1:8080/

-c100 connections, -d30s duration (wrk's default is 20s — run at least that long, ideally after a short warm-up, for a steady-state number), and --latency for the percentile distribution. Read Requests/sec for throughput, and the Latency percentiles for the tail.

On a Ryzen 9 9950X3D over loopback, this single-file server sustained roughly:

  Latency Distribution
     50%  156.00us
     75%  159.00us
     90%  161.00us
     99%  167.00us
  Requests/sec: 406762

That's the point: p99 sits within ~1.1× of p50 — essentially a flat tail (167µs vs 156µs) — because with no garbage collector there are no pauses to stretch it. Absolute numbers depend on your hardware, NIC, and load generator, so benchmark the box you care about; but you'll be measuring a serving path with nothing between the request and the metal.

But that ~407k is not the server's ceiling — it's wrk's. See the next step.

Step 8 — wrk is the bottleneck: measure with SuperJ's own client

wrk is a single process, and against a server this fast it becomes the bottleneck itself — you end up measuring wrk, not the server. SuperJ ships a native load generator for exactly this: demo/sj/demo/WebClient.sj, an event-loop client on the same built-in stack. It opens keep-alive connections and pipelines requests, so one process drives far more load than wrk can.

Compile it too, then point it at the running server — webclient <port> [connections] [pipeline] [seconds]:

superj compile "$SJ_HOME/demo/sj/demo/WebClient.sj" --sdk-path "$SJ_HOME/sdk" --link --output webclient
./webclient 8080 64 16 15          # 64 connections, 16 pipelined requests each, 15s

On the same box (Ryzen 9 9950X3D, server pinned to one core, load generator to another), a single wrk process topped out around 423k req/s, while one WebClient process pushed the same server to ~515k req/s — the server was never the limit; the client was. Run several WebClient processes (or add pipelining) for still more. Lesson: when you're benchmarking something this fast, check that your load generator isn't the thing you're actually measuring.

Step 9 — Scale across cores

Single-threaded doesn't mean single-core: scale with processes, not threads. The true in NioConfig is reusePort — multiple processes can share the same port, and on Linux the kernel balances incoming connections across them. Run several copies:

for i in 1 2 3 4; do ./webserver 8080 & done

No code change — the same binary, more copies, more cores serving.


Why it's this fast

Going further

The same stack does more than plaintext:

Browse the full HTTP API with superj doc --list (or superj doc sj.http.Router).

SuperJ — manual · generated from fastest-web-server.md at pack time