Orisha Runs on macOS, Linux, and No Operating System At All

· 10 min read

In January, Orisha could serve a page. It had a route table computed at compile time, a single event loop, and a benchmark. It was a demonstration that a web server could be mostly compiler.

It now serves this website. Not a copy of it — this one, the pages you are reading, all 1,123 files of it, byte for byte. And it does that from four different platforms, one of which is not an operating system.

This is what changed.

The program

Here is the entire program that serves korulang.org:

import orisha

orisha:static(name: "site", root: "../../../korulang_org/build", fallback: "200.html")
orisha:handler = orisha:static-router(name: "site")
orisha:serve(port: 3000)

Three declarations. The first says which directory is the website. The second says requests are answered from it. The third starts the server.

There is no routing table in that file, no MIME type list, no cache headers, no ETag logic, no compression step, and no file reads at runtime — because all of that happened while it compiled. The website is inside the executable.

The whole framework behind it is about 2,500 lines.

Four platforms, one server

A web server’s event loop is the part that has to change per platform, and it is also the part everyone copies. Orisha has four of them, and they share everything except the waiting:

  • kqueue — macOS and the BSDs
  • epoll — Linux
  • io_uring — Linux 5.1 and later
  • unikraft — a unikernel, with no operating system at all

They are not four servers. Request framing, connection buffering, keep-alive, the idle timeout, response writing, the router, the compile-time embedding — all of that is written once and none of it knows which platform it is on. What each platform supplies is the loop body, and choosing one is a build flag:

koruc main.k                     # kqueue
koruc main.k --build=linux      # epoll
koruc main.k --build=io-uring   # io_uring
koruc main.k --build=unikraft   # unikernel

Adding a fifth platform is one loop body and one line of configuration. Nothing above it moves. We know because io_uring was added that way after the seam existed, and the diff touched nothing else.

That is worth dwelling on, because io_uring is not a faster spelling of epoll — it is inverted. epoll answers “which of these connections has data” and then you read it. io_uring is told the read already happened and hands you the result. One is a hint, the other is a completion. The seam absorbed that difference without anything above it noticing.

The unselected loops are not merely unused. They are not in the binary.

The one with no operating system

The fourth platform is the one worth the most words.

A unikernel is an application and just enough kernel to run it, linked into a single bootable image. There is no process model, no shell, nothing else running. Our image boots under QEMU, brings up a network interface, gets an address by DHCP, and serves this website.

It needed its own loop, and not for style. On that target there is no operating system to ask for sockets, so the socket calls are declared directly against what the image actually contains. It has one core and no second thread to spawn, so there is nothing to multiplex work onto. And its scheduler is cooperative rather than preemptive, which rules out the worker pool the hosted platforms use.

The seam is what made that survivable. A unikernel is not a port of the server; it is one loop body, next to the others.

Two things broke on the way there, and both were interesting. The first was that the memory allocator went through the operating system — it asked for pages, and there is no one to ask. So a freestanding build now carves a fixed arena out of the image itself. The second was a clock: the response Date header needed the time, and reaching for it the usual way needs headers that a freestanding target does not have.

Neither is exotic. Both are the same shape: a thing you assume is there, and on this target it simply is not.

Measured today: a 26,487,336-byte bootable image, serving all 1,123 files byte-identical, verified against the originals on disk.

Everything decided while it compiles

The reason a request is cheap is that almost nothing is left to do when one arrives.

At compile time, Orisha walks the program, finds the directory you named, reads every file in it, computes an ETag for each, compresses them, works out the content types, and builds the complete HTTP response — status line, headers and body — as one constant.

At runtime, answering a request is: parse the path, look up the constant, write it to the socket.

There is no filesystem call, no header formatting, no string concatenation and no allocation on that path. The website is not served from the executable, it is the executable.

That has a real cost, and it is the honest counterweight: publishing a post means recompiling. A content change is a rebuild and a redeploy, not a file sync. For a site that changes a few times a week, that is a good trade. For one that changes every few minutes, it would not be.

Speed, measured this week

One worker each against nginx 1.29.1, with sendfile, tcp_nodelay, tcp_nopush and open_file_cache on, both servers sending byte-identical compressed responses, 200 connections:

Response sizeOrishanginxRatio
1,145 bytes103,000 req/s78,000 req/s1.32x
13,341 bytes129,800 req/s74,400 req/s1.75x

With all twelve cores on both sides, Orisha holds 147,700 req/s against nginx’s 118,000 on the larger file — 1.25x — and keeps a better tail: 1.70ms at the 99th percentile against 2.19ms.

Two caveats worth stating rather than burying. The all-cores figure is measured with the load generator on the same twelve cores as the servers, so both are competing with the thing measuring them; read it as a floor. And there is one result we cannot explain — throughput is higher on the 13 KB file than the 1 KB one, which is backwards, and nginx does not do it. The ratios reproduced across three files and every round, so we are confident in those; we are not claiming anything about how absolute throughput scales with payload size.

The January post reported a larger lead. Part of that is that the server measured then answered only the first request in each packet and abandoned large responses half-written — both fixed since, and both fixes cost work on every request. The old number was faster the way that not doing the work is always faster.

Serving to the actual internet

Orisha speaks plain HTTP and has no HTTPS of its own. That is a real gap, and the shape it takes in production is the ordinary one: something in front holds the certificate and speaks TLS to the world, and Orisha serves behind it on a private network.

Verified this week, end to end: every one of the 1,123 files byte-identical over HTTPS on a single connection, TLS 1.3, HTTP/2 negotiated, plain HTTP answered with a permanent redirect. The container it runs in has no operating system in it — the binary is statically linked and the site is inside it, so the image is FROM scratch and one file, 25,143,240 bytes.

It cross-compiles in one command. A Linux x86-64 executable, built from a Mac, with a 66 MB website inside it.

What it still cannot do

  • No TLS of its own. Something in front has to terminate it.
  • Publishing means recompiling, as above.
  • It compresses responses whether or not the client said it could handle them. A client that cannot decompress gets bytes it cannot read. This is a bug, it is known, and it is next.
  • io_uring and the unikernel each run a single loop. The hosted platforms run one worker per core; io_uring’s inverted readiness and the unikernel’s cooperative single-core scheduler each need their own answer, and neither has one yet.

The shape of the thing

What is interesting here is not that a web server is fast. Plenty are.

It is that four platforms — including one with no operating system beneath it — share a single implementation of everything that is not waiting for a socket, and that adding the fourth did not disturb the other three. And that the program a person writes to use it is three declarations, because the work that a server normally does per request was done once, while it compiled.

The server used to be a demonstration. It now runs the site you are reading it on.