32KB binary

⚡ U WebServer

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.

Measured performance

Single-threaded, single binary, Ubuntu 24.04. All numbers from actual benchmarks.

TestConnectionsReq/sLatencyFailures
Sequential (no keep-alive)121,0900.05ms0
Keep-alive100103,5470.010ms0
Keep-alive (warm cache)100104,5760.010ms0
900 concurrent90085,6870.012ms0

290,000 requests. Zero failures. 100% cache hit rate. Memory stable at 18MB.

vs nginx, PHP, and the Qbix Server

nginx (1 worker)Qbix PHP ServerU WebServer
Keep-alive req/s80K–120K36,300104,000
Handler / PHP req/s3K (via fpm)8K (fork)104,000
Memory2MB + workers30MB + forks18MB total
Per-request costFastCGI roundtripfork (~500μs)function call (~100ns)
Binary1.3MB280KB PHAR32KB

How it works

Drop .u files in a folder. They handle that URL path. No routing config, no middleware chain.

web/api/echo.u
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).

Why no fork()?

PHP forks a process per request because it has no isolation model. U's -E modifier provides the same isolation through the type system:

isolation comparison
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.

Merkle-tree cache invalidation

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.

response headers from handler
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.

WebSocket + rooms

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.

Features

Static filesETag, 304 Not Modified, LRU cache, sendfile for large files, directory listing
Keep-aliveHTTP/1.1, TCP_NODELAY, connection reuse
TLSOpenSSL 3.x, TLSv1.3, AES-256-GCM, ALPN, cert hot-reload
WebSocketRFC 6455 + Socket.IO v5 (Engine.IO handshake, ping/pong, events, acks)
RoomsShared state, tick timer, broadcast, join/leave
Component cacheMerkle-tree invalidation from response headers
Compressiongzip/deflate for text >1KB
SecurityPath traversal blocked, dotfiles blocked
Multi-coreSO_REUSEPORT — one event loop per core, linear scaling

Quick start

git clone https://github.com/ULanguageOrg/webserver.git
cd webserver
./bin/uwebserver --port=8080

The repo includes a starter project in web/ with an index page, example handlers, and CSS. Full documentation on GitHub.