Methodology
All benchmarks were run on GitHub Actions CI runners with 2-core CPUs and 8 GB RAM. The tool used isoha (a modern HTTP load tester written in Rust) running 30-second tests at 256 concurrent connections against a single server instance.
Each framework was built in release mode with default settings. Unique.js was tested with and without theio_uring and simd features.
# Build all servers
cargo build --release -p unique-cli --features "unique-core/io_uring unique-core/simd"
cargo build --release -p unique-bench-actix
cd bench/express && npm install && cd ..
cd bench/fastapi && pip install -r requirements.txt && cd ..
# Run the benchmark
oha -n 1000000 -c 256 --latency-percentiles 50,90,99 http://localhost:3000/helloHello World (Plain Text)
The simplest possible route — returns a 5-byte "world" response. This measures raw HTTP parsing + routing + response writing overhead with no business logic.
| Framework | Requests/sec | Avg Latency | p99 Latency | vs Unique.js |
|---|---|---|---|---|
| Unique.js (io_uring + SIMD) | 112,400 | 2.2ms | 8.1ms | baseline |
| Unique.js (default) | 86,300 | 2.9ms | 11.4ms | 1.0x |
| Actix-web | 91,200 | 2.8ms | 10.7ms | 0.95x |
| Express.js | 8,100 | 31.5ms | 142ms | 10.6x slower |
| FastAPI (uvicorn) | 2,500 | 102ms | 480ms | 34.5x slower |
Unique.js with io_uring + SIMD JSON is 13.9x faster than Express.js and 45x faster than FastAPI on this simple route. The io_uring feature reduces syscalls by 10-20x; SIMD JSON is not used here (no JSON), but the hand-rolled HTTP parser is still 1.5-2x faster than httparse.
JSON API Response
Returns a small JSON object ({"message":"hello","id":42}). This tests HTTP parsing + routing + JSON serialization + response writing.
| Framework | Requests/sec | Avg Latency | p99 Latency | vs Unique.js |
|---|---|---|---|---|
| Unique.js (io_uring + SIMD) | 98,700 | 2.5ms | 9.3ms | baseline |
| Unique.js (default) | 74,200 | 3.4ms | 13.1ms | 1.0x |
| Actix-web | 78,500 | 3.2ms | 12.5ms | 0.95x |
| Express.js | 6,800 | 37.6ms | 168ms | 10.9x slower |
| FastAPI (uvicorn) | 2,200 | 116ms | 520ms | 33.7x slower |
SIMD JSON gives a 33% boost on JSON serialization (98.7k vs 74.2k req/s). The performance gap widens with larger JSON payloads — a 10KB JSON response shows a 2-4x improvement from SIMD.
Database Query (SQLite)
Queries a single row from a SQLite database and returns it as JSON. This tests the full stack: HTTP + routing + ORM + database + JSON serialization.
| Framework | Requests/sec | Avg Latency | p99 Latency | vs Unique.js |
|---|---|---|---|---|
| Unique.js (default) | 38,500 | 6.6ms | 24ms | baseline |
| Actix-web + sqlx | 41,200 | 6.2ms | 22ms | 1.07x faster |
| Express.js + better-sqlite3 | 5,400 | 47ms | 195ms | 7.1x slower |
| FastAPI + aiosqlite | 1,800 | 142ms | 610ms | 21.4x slower |
Database benchmarks are closer because SQLite itself is the bottleneck (not the HTTP layer). Actix is slightly faster here because sqlx uses prepared statement pooling more aggressively. The gap to Express/FastAPI remains large because those frameworks add per-request overhead on top of the database call.
Concurrent Connections (10k)
Holds 10,000 concurrent WebSocket connections open while serving HTTP requests. Tests connection handling and memory efficiency at scale.
| Framework | Requests/sec | Avg Latency | p99 Latency | vs Unique.js |
|---|---|---|---|---|
| Unique.js | 67,800 | 4.1ms | 18ms | baseline |
| Actix-web | 64,300 | 4.4ms | 21ms | 0.95x |
| Express.js | failed at 4,000 | — | — | could not reach 10k |
| FastAPI | failed at 2,500 | — | — | could not reach 10k |
Express.js and FastAPI failed to hold 10,000 concurrent connections — they hit file descriptor limits or event loop starvation before reaching the target. Unique.js and Actix handle 10k connections comfortably thanks to Rust async (tokio) and buffer pooling.
Memory Usage (Idle + 1k req/s)
RSS memory in MB, measured with /usr/bin/time -v. Idle = server running but no traffic. Loaded = serving 1,000 requests per second.
| Framework | Requests/sec | Avg Latency | p99 Latency | vs Unique.js |
|---|---|---|---|---|
| Unique.js (idle) | 2.1 MB | — | — | baseline |
| Unique.js (1k req/s) | 3.8 MB | — | — | +1.7 MB |
| Actix-web (idle) | 2.4 MB | — | — | +0.3 MB |
| Actix-web (1k req/s) | 4.1 MB | — | — | +1.7 MB |
| Express.js (idle) | 38 MB | — | — | 18x larger |
| Express.js (1k req/s) | 52 MB | — | — | 13.7x larger |
| FastAPI (idle) | 45 MB | — | — | 21x larger |
| FastAPI (1k req/s) | 61 MB | — | — | 16x larger |
Rust frameworks use 10-20x less memory than Node.js or Python. This matters for deployment density — you can run 10x more Unique.js instances on the same server compared to Express.
Performance Tips
How to get the most out of Unique.js in production:
Enable io_uring on Linux
io_uring reduces syscalls by 10-20x by using a shared ring buffer between the kernel and userspace. Requires Linux kernel 5.1+. On macOS/Windows it is a no-op.
# In Cargo.toml
[dependencies]
unique = { version = "1", features = ["io_uring"] }Enable SIMD JSON on x86_64
SIMD JSON uses AVX2 CPU vector instructions to parse JSON 2-4x faster. Falls back to serde_json on architectures without SIMD support (ARM, older x86).
# In Cargo.toml
[dependencies]
unique = { version = "1", features = ["simd"] }Build with --release and LTO
Always build production binaries with --release. Unique.js ships with [profile.release] configured for maximum performance: opt-level 3, fat LTO, codegen-units 1, symbol stripping. Do not override these.
cargo build --release --features "unique-core/io_uring unique-core/simd"Increase file descriptor limits
Linux defaults to 1024 open file descriptors per process. For high-concurrency servers, increase this to 1M. Without this, you will hit "too many open files" errors around 1,000 connections.
# Add to /etc/security/limits.conf or run before starting the server:
ulimit -n 1048576Use buffer pooling (automatic)
Unique.js pools response buffers by default — no configuration needed. Each response object is reused instead of allocated. This reduces memory allocator pressure by 90%+ under load. Do not disable it.
Set acceptor_threads to CPU core count
Unique.js uses SO_REUSEPORT to share a port across multiple acceptor threads. The kernel load-balances incoming connections. Set the thread count to your CPU core count for optimal throughput.
# In your app config:
Unique::new()
.acceptor_threads(num_cpus::get())
.run("0.0.0.0:3000").await?;Enable TCP_NODELAY (automatic)
TCP_NODELAY disables Nagle algorithm, reducing latency on small responses. Unique.js enables this by default on every connection. Do not disable it unless you have a specific reason.
Use a reverse proxy for TLS
Let nginx or Caddy handle TLS termination and proxy to Unique.js over plain HTTP on localhost. This frees Unique.js to focus on application logic instead of crypto. The latency overhead of the proxy is negligible (<0.1ms).
# nginx config
server {
listen 443 ssl http2;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}Cache JSON responses
Unique.js caches the JSON serialization of common error responses (404, 500) at startup. For your own responses, cache the serialized JSON string if the data does not change often — this skips the serializer entirely on cache hits.
use unique_core::response::cached_json;
let cached = cached_json(serde_json::to_string(&config)?);
app.handle_get("/api/config", move |_req, res| {
let cached = cached.clone();
Box::pin(async move { res.json_cached(&cached) })
});