32KB binary
A native web server compiled from U. Epoll event loop, HTTP/1.1 keep-alive, TLS, in-memory response cache, sendfile, Merkle-tree component invalidation, WebSocket with Socket.IO, and rooms — all from a single 32KB binary with zero dependencies.
Inspired by the Qbix PHP Server, which proved that an in-process cache can beat nginx on keep-alive. U takes the same architecture and removes every remaining bottleneck.
Single-threaded, single binary, Ubuntu 24.04. All numbers from actual benchmarks.
| Test | Connections | Req/s | Latency | Failures |
|---|---|---|---|---|
| Sequential (no keep-alive) | 1 | 21,090 | 0.05ms | 0 |
| Keep-alive | 100 | 103,547 | 0.010ms | 0 |
| Keep-alive (warm cache) | 100 | 104,576 | 0.010ms | 0 |
| 900 concurrent | 900 | 85,687 | 0.012ms | 0 |
290,000 requests. Zero failures. 100% cache hit rate. Memory stable at 18MB.
| nginx (1 worker) | Qbix PHP Server | U WebServer | |
|---|---|---|---|
| Keep-alive req/s | 80K–120K | 36,300 | 104,000 |
| Handler / PHP req/s | 3K (via fpm) | 8K (fork) | 104,000 |
| Memory | 2MB + workers | 30MB + forks | 18MB total |
| Per-request cost | FastCGI roundtrip | fork (~500μs) | function call (~100ns) |
| Binary | 1.3MB | 280KB PHAR | 32KB |
Drop .u files in a folder. They handle that URL path. No routing config, no middleware chain.
f handle(req: Request) -> Response
msg = req.query.get("msg") ?? ""
msg.len == 0 ? r => Response.json({ "error": "missing msg" })
r => Response.json({ "echo": msg, "server": "U WebServer" })The ?? is U's null-coalescing operator. get() returns +N (nullable) — a missing key is just none, not an exception. The x operator is reserved for actual errors (I/O failures, parse errors).
PHP forks a process per request because it has no isolation model. U's -E modifier provides the same isolation through the type system:
PHP: fork() → run script → exit() ~500μs + 15MB COW pages U: call handler(req) → return Resp ~100ns + 0 bytes allocated
A handler declared +E(db) can only access the database through the injected db capability. No filesystem, no network, no globals. The compiler verifies it. Same safety as fork, 5,000x cheaper.
Most caches store whole pages. When anything changes, you purge everything. U WebServer (like the Qbix PHP Server it was ported from) caches page components — header, feed, sidebar — and only invalidates what changed.
Handlers return response headers describing their components. The server stores a Merkle tree of content hashes (~200 bytes per page). When a dependency changes, only the affected leaf is marked stale.
X-Cache-Tree: {"l":{"feed":"a3f2","sidebar":"b8c1","header":"d4e5"}}
X-Cache-Deps: {"feed":["community/123/feed"],"sidebar":["community/123/about"]}Stream community/123/feed updates → only the "feed" component invalidates. Sidebar and header stay cached.
Built-in WebSocket with Socket.IO v5 wire protocol. Standard socket.io-client connects without changes. Rooms are data structures in the event loop — not separate processes — so one server can manage 100,000 rooms where PHP would run out of memory at 1,600.
| Static files | ETag, 304 Not Modified, LRU cache, sendfile for large files, directory listing |
| Keep-alive | HTTP/1.1, TCP_NODELAY, connection reuse |
| TLS | OpenSSL 3.x, TLSv1.3, AES-256-GCM, ALPN, cert hot-reload |
| WebSocket | RFC 6455 + Socket.IO v5 (Engine.IO handshake, ping/pong, events, acks) |
| Rooms | Shared state, tick timer, broadcast, join/leave |
| Component cache | Merkle-tree invalidation from response headers |
| Compression | gzip/deflate for text >1KB |
| Security | Path traversal blocked, dotfiles blocked |
| Multi-core | SO_REUSEPORT — one event loop per core, linear scaling |
The repo includes a starter project in web/ with an index page, example handlers, and CSS. Full documentation on GitHub.