Skip to Content
HTTP & Performance

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:

PlatformBackend
Linux and Androidepoll
macOS and FreeBSDkqueue
Windows and unsupported targetsblocking 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.

PlatformHardwareOperating systemqaws automatic runtime
macOS ARM6410-core heterogeneous ARM64, 16 GiB memorymacOS 26.5.2, Darwin 25.5.0, arm644 workers using kqueue
mobile Linux ARM648-core heterogeneous ARM64 (2 performance + 6 efficiency), about 5.5 GiB memorypostmarketOS edge (Alpine-based), Linux 6.14.7, aarch642 high-capacity workers using epoll
Linux x86_646 cores/6 threads, about 15 GiB memoryDebian 13.5, Linux 6.17.13-2-pve, x86_646 workers using epoll

Linux reported capacities 322 for the six efficiency cores and 1024 for the two performance cores on the mobile Linux ARM64 system, which is why automatic mode selected two workers. The Linux x86_64 system 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 platform 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.

Platform-c1-c10-c100-c1000
macOS ARM64-0.81%+4.03%+4.77%+2.83%
mobile Linux ARM64+5.05%+0.74%+22.76%-4.11%

Automatic topology detection selected the two 1024-capacity cores on the mobile Linux ARM64 system. Its 1 MiB and 64 MiB sendfile medians were about 6-7% below 0.2.6, while cached -c100 improved substantially. On macOS ARM64, 1 MiB and 64 MiB transfer medians improved about 13% and 4%. This is an explicit throughput tradeoff observed on those systems, 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.

PlatformqawsnginxCaddy
macOS ARM64200.7k / 181.0k / 7.24 GiB/s84.7k / 79.6k / 6.76 GiB/s47.3k / 41.1k / 6.79 GiB/s
mobile Linux ARM64166.7k / 122.1k / 10.20 GiB/s73.3k / 70.9k / 11.68 GiB/s24.4k / 22.8k / 11.38 GiB/s
Linux x86_64298.5k / 322.2k / 10.10 GiB/s125.3k / 127.9k / 10.17 GiB/s54.1k / 57.4k / 9.91 GiB/s

The comparison used nginx 1.31.2 and Caddy 2.11.4 on macOS ARM64, nginx 1.30.3 and Caddy 2.11.4 on mobile Linux ARM64, and nginx 1.26.3 and Caddy 2.6.2 on Linux x86_64. 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.bin

Repeat 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.

qaws docs. Built for static hosting.