HTTP And Performance
qaws 0.2.7 is optimized for static HTTP throughput while keeping backend
selection internal. The common cached path avoids blocking I/O, shared
cached-hit locks, per-request allocation, and access-log work when logging is
disabled.
Event engine
qaws selects its runtime backend automatically:
| Platform | Backend |
|---|---|
| Linux and Android | epoll |
| macOS and FreeBSD | kqueue |
| Windows and unsupported targets | blocking worker pool |
On evented platforms, each worker normally owns a nonblocking SO_REUSEPORT
listener, event descriptor, serve-root handle, cache view, and its connection
state. If multiple listeners are unsupported, qaws falls back to the
main-thread accept dispatcher without exposing a backend setting.
--workers <n> remains the explicit override. Automatic mode selects the
highest-capacity CPU cluster on Linux/Android, uses macOS performance-level
CPU information when available, and falls back to the logical CPU count. qaws
does not pin worker threads.
Resumable responses
Memory responses, buffered files, and sendfile bodies all use resumable
per-connection output state. qaws tracks header/body offsets, enables write
readiness only while output is pending, and resumes after EAGAIN. Read
readiness is disabled while a response is pending so later pipelined responses
cannot overtake it.
Connections that make no write progress for keep_alive_timeout_ms are
closed. Client resets and broken pipes during body transfer are treated as
normal disconnects rather than noisy internal failures.
Keep-alive and pipelining
HTTP/1.1 keeps connections alive by default. HTTP/1.0 closes by default unless
the client requests keep-alive. Connection: close always closes after the
current response.
Pipelined requests are processed sequentially and responses remain ordered. Request storage uses start/end offsets rather than copying leftovers after each request. An internal 16-request batch keeps busy pipelines fair; buffered work is requeued internally instead of waiting for another socket event.
HTTP/1.1 requires a nonempty Host. qaws rejects malformed framing,
conflicting Content-Length, positive request bodies, every
Transfer-Encoding, and oversized headers. A malformed pipelined request
closes defensively after an error response when possible.
Cache V2
The small-file cache is enabled by default. It uses shared immutable, reference-counted generations and worker-local views. Established cached hits do not take the global slot-map lock. Refresh locking is per path, so one file reload does not block unrelated paths.
After revalidate_ms, qaws stats the file first. Unchanged size and nanosecond
mtime extend the TTL without rereading the body. Changed files receive a new
generation while in-flight responses retain the old generation until their
references are released.
The memory limit accounts for bodies, paths, headers, response storage, and
live old generations. If a new entry cannot fit, qaws serves it uncached; there
is no LRU/CLOCK eviction in 0.2.7.
When access logging is disabled, qaws also skips access-record construction, user-agent extraction, remote-address formatting, and per-request timing.
Sendfile
Sendfile remains enabled by default for uncached regular-file GET bodies on
supported Unix platforms. Headers are written first, then the file body is
resumed through the event output state. Unsupported platforms and failures
before body bytes are sent use buffered streaming.
Cached files, HEAD, conditional 304, redirects, and errors do not use the
sendfile body path. Byte ranges can use cached, buffered, or sendfile transfer
according to the selected representation.
Benchmark systems
The published measurements came from three very different machines. Every HTTP load generator targeted loopback, so the results measure the server and local TCP stack rather than LAN or Internet throughput.
| Host | Hardware | Operating system | qaws automatic runtime |
|---|---|---|---|
| MacBook Air | Apple M4, 10 cores (4 performance + 6 efficiency), 16 GB unified memory | macOS 26.5.2, Darwin 25.5.0, arm64 | 4 workers using kqueue |
lite114 | Qualcomm SM7125 ARM phone, 2 Kryo Gold cores up to 2.323 GHz + 6 Kryo Silver cores up to 1.805 GHz, about 5.5 GiB RAM | postmarketOS edge (Alpine-based), Linux 6.14.7, aarch64 | 2 capacity-1024 workers using epoll |
local01 | Intel Core i5-8500T, 6 cores/6 threads up to 3.5 GHz, about 15 GiB RAM | Debian 13.5, Linux 6.17.13-2-pve, x86_64 | 6 workers using epoll |
Linux reported capacities 322 for the six efficiency cores and 1024 for
the two performance cores on lite114, which is why automatic mode selected
two workers. local01 was under substantial unrelated memory pressure during
validation: roughly 12 GiB of RAM and 5.7 GiB of its 8 GiB swap were in use.
It was retained as an observational comparison host rather than a release
throughput gate.
The cached fixture was a 268-byte HTML document. Transfer tests used sparse
1 MiB and 64 MiB files. Primary qaws comparisons used the native release
artifact for each architecture, access logs disabled, and a file-descriptor
limit of 65536.
Reproducible benchmark method
Use the same machine, file, binary options, and file-descriptor limit. Primary throughput runs disable access logs:
ulimit -n 65536
qaws --host 127.0.0.1 --port 18086 --serve ./public --no-access-log
wrk -t1 -c1 -d10s http://127.0.0.1:18086/
wrk -t1 -c10 -d10s http://127.0.0.1:18086/
wrk -t8 -c100 -d10s http://127.0.0.1:18086/
wrk -t8 -c1000 -d10s http://127.0.0.1:18086/The release gate alternated 0.2.6 and 0.2.7 for five repetitions and
compared medians. The fixture was the same cached 268-byte HTML file. Every
listed run completed without socket errors or timeouts.
| Host | -c1 | -c10 | -c100 | -c1000 |
|---|---|---|---|---|
| Apple Silicon MacBook Air | -0.81% | +4.03% | +4.77% | +2.83% |
lite114 PostmarketOS ARM | +5.05% | +0.74% | +22.76% | -4.11% |
Automatic topology detection selected the two 1024-capacity cores on
lite114. Its 1 MiB and 64 MiB sendfile medians were about 6-7% below
0.2.6, while cached -c100 improved substantially. On the Mac, 1 MiB and
64 MiB transfer medians improved about 13% and 4%. This is an explicit
throughput tradeoff observed on those machines, not a universal prediction.
nginx and Caddy comparison
A separate five-repetition, rotating-order matrix used five-second runs, the
same fixtures, and disabled access logging. Values below are medians shown as
c100 req/s / c1000 req/s / 64 MiB transfer.
| Host | qaws | nginx | Caddy |
|---|---|---|---|
| MacBook Air | 200.7k / 181.0k / 7.24 GiB/s | 84.7k / 79.6k / 6.76 GiB/s | 47.3k / 41.1k / 6.79 GiB/s |
lite114 | 166.7k / 122.1k / 10.20 GiB/s | 73.3k / 70.9k / 11.68 GiB/s | 24.4k / 22.8k / 11.38 GiB/s |
local01 | 298.5k / 322.2k / 10.10 GiB/s | 125.3k / 127.9k / 10.17 GiB/s | 54.1k / 57.4k / 9.91 GiB/s |
The comparison used nginx 1.31.2 and Caddy 2.11.4 on the Mac, nginx
1.30.3 and Caddy 2.11.4 on lite114, and nginx 1.26.3 and Caddy 2.6.2
on local01. All measured runs reported zero socket errors. Loopback results
are useful for regression analysis, not promises for production networks.
Additional diagnostics
Force one request per connection to measure connection churn:
qaws --host 127.0.0.1 --port 18086 --serve ./public --no-access-log --no-keep-alive
wrk -t8 -c100 -d10s http://127.0.0.1:18086/Create large files for sendfile comparisons:
truncate -s 1m public/one-mib.bin
truncate -s 64m public/sixty-four-mib.bin
wrk -t4 -c16 -d30s http://127.0.0.1:18086/one-mib.bin
wrk -t4 -c16 -d30s http://127.0.0.1:18086/sixty-four-mib.binRepeat with --no-sendfile, a cache-disabled JSON config, enabled access logs,
idle keep-alive clients, and throttled large downloads to isolate each path.
Not implemented
qaws does not provide runtime compression, multipart range responses, TLS, authentication, directory listings, SPA fallback, filesystem watchers, immutable cache mode, LRU/CLOCK eviction, worker affinity, io_uring, or IOCP.