I've released the first version of ruxen, an experimental nginx-compatible HTTP server and reverse proxy written from scratch in Rust.
- GitHub: GitHub - gvozdetsky/ruxen: Experimental nginx-compatible HTTP server and reverse proxy written in Rust · GitHub
- crates.io: crates.io: Rust Package Registry
- Release notes: Release ruxen v0.1.0 · gvozdetsky/ruxen · GitHub
The project started mostly because I wanted to understand nginx better. I've been reading its C source — the event loop, HTTP request state machine, location matching, request phases, upstream handling, configuration inheritance — and then trying to reproduce the behaviour and architectural ideas in Rust.
It isn't a line-by-line port. A lot of nginx's implementation machinery exists because C needs it: memory pools, pointer-heavy configuration structures, arrays of function pointers, manual lifetime management, and so on. Where Rust already solves the underlying problem differently, ruxen uses the Rust approach instead. The goal is to preserve useful nginx semantics, not its implementation details.
I also deliberately didn't build it on Tokio or hyper. That's not because I think there is anything wrong with them — using hyper would obviously be the sensible choice if the goal were simply to build an HTTP server. But in this project, implementing the HTTP/1.1 parser, request lifecycle and response path is part of the experiment itself.
ruxen is Linux-only and uses monoio, with a thread-per-core worker model, SO_REUSEPORT and io_uring.
By "nginx-compatible", I currently mean a useful and growing subset of nginx configuration and HTTP behaviour. ruxen already handles things like virtual servers and location matching, static files, try_files, rewrites, variables, TLS termination with rustls, proxy_pass, upstream groups, keep-alive pools, weighted round-robin, least_conn, failover and a number of nginx-style CLI options.
It is not intended to be a drop-in nginx replacement at this stage.
Testing against nginx's own test suite
One part of the project that has turned out to be especially useful is testing ruxen against nginx's upstream nginx-tests suite.
ruxen currently advertises enough nginx functionality for 104 test files to apply. Of those, 49 pass end-to-end and 55 still have one or more failing subtests. Another 401 files are skipped because they exercise modules or protocols that ruxen doesn't claim to support, such as HTTP/2, stream, mail and caching.
The detailed status is here:
https://github.com/gvozdetsky/ruxen/blob/main/NGINX_TEST_PROGRESS.md
The nginx tests have caught quite a few details that I probably wouldn't have thought to test myself — Host handling, URI normalization, header behaviour, error-page redirects, keep-alive edge cases, and subtle differences in configuration inheritance.
They've also influenced what I implement next: instead of guessing which compatibility feature matters, a failing upstream test gives me a concrete behaviour to investigate.
Performance
I've also been benchmarking ruxen against nginx 1.24.0 throughout development.
A single benchmark run on a laptop turned out to drift by up to ±10% because of things like CPU boost after idle and thermal state, so the release numbers come from interleaved A/B pairs — nginx, ruxen, ruxen, nginx, … — using the geometric mean of the per-pair throughput ratios.
| workload | ruxen / nginx |
|---|---|
return 200, keep-alive |
97.6% |
| static 1 KiB file, keep-alive | 102.4% |
| static 1 KiB file, no keep-alive | 90.7% |
proxy_pass with upstream keep-alive |
96.6% |
auth_basic |
105.3% |
TLS 1.3 return 200 |
110.7% |
Across the full 22-scenario release suite, 19 scenarios are at or above 95% of nginx's throughput.
The current weak point is short-lived connections: without keep-alive, ruxen has higher per-connection setup and teardown costs than nginx's epoll-based worker.
All of this is from one laptop (i9-13900HX), with client and server running on the same machine over loopback, so I consider these results indicative rather than a general claim that ruxen is faster or slower than nginx.
The full table and methodology are in the release notes.
A few things I learned along the way
-
returnruns beforeauth_basic.returnis a rewrite-phase directive, solocation / { auth_basic "x"; return 200; }answers 200 without asking for credentials. While setting up theauth_basicbenchmark, I found that ruxen was getting this wrong: nginx returned 200 while ruxen returned 401. -
Linux threads normally share an fd table; nginx worker processes don't. ruxen uses worker threads, so every
open/closeon the static-file path originally contended on the same kernel fd-table lock. Giving each worker thread its own table withunshare(CLONE_FILES)improved the 304 benchmark by 4.7%. -
Small configuration details matter. Right before the release, a Docker test showed that
return 200 "ok\n";sent a literal backslash andn: nginx unescapes\t,\rand\nin configuration strings, while ruxen didn't.
Limitations
There are still many deliberate limitations in 0.1.0:
- HTTP/1.1 only
- Linux only
- no HTTP/2 or HTTP/3
- no nginx module ecosystem
- no HTTP cache
- no TLS to proxy upstreams
- no nginx-style configuration reload without restart
- nothing like nginx's full master-process supervision and live binary upgrade machinery
I would definitely not recommend using ruxen as a production replacement for nginx yet.
Trying it
From crates.io:
cargo install ruxen --locked
cargo install builds from source, so it needs Rust 1.88+, plus cmake and a C compiler for the aws-lc-rs crypto backend used by rustls.
There is also a prebuilt x86_64 Linux binary on the release page.
ruxen needs a Linux kernel with io_uring available. Docker's default seccomp profile blocks the io_uring syscalls, so for a quick local test run the container with --security-opt seccomp=unconfined:
docker run --security-opt seccomp=unconfined ...
A custom seccomp profile allowing the required syscalls is a better option for anything beyond local experimentation.
A minimal nginx-style configuration (ruxen.conf):
worker_processes 1;
events {
worker_connections 1024;
}
http {
server {
listen 8080;
location / {
return 200 "hello from ruxen\n";
}
}
}
Then:
ruxen -t -c ruxen.conf
ruxen -c ruxen.conf
curl -i http://127.0.0.1:8080/
Feedback welcome
I'm particularly interested in feedback from people who know nginx internals, HTTP servers, io_uring, or thread-per-core runtimes.
Compatibility differences with nginx are especially useful — even a small reproducible configuration where nginx and ruxen behave differently is a good issue to open.
If you'd like to contribute, there are a few open issues to start from, including running the benchmarks on different hardware and some smaller nginx-compatibility gaps:
The code, design notes, benchmark setup and contribution guide are all in the repository.