Educational, production-oriented HTTP/1.1 server in C for Linux using non-blocking sockets + edge-triggered epoll.
- Single binary:
httpd - Multi-threaded event loops (
-t N) withSO_REUSEPORT - One epoll fd + one connection table per worker thread
- Correct ET handling: read/write loops drain until
EAGAIN - HTTP/1.1 request line + headers parsing with incremental reads
- Keep-alive by default;
Connection: closehonored Content-Lengthbody support forPOST /echo- Routes:
GET /healthz->okPOST /echo-> echoes request bodyGET /static/<path>-> static files viasendfile()GET /metrics-> Prometheus-style text metrics (requests_total,requests_per_sec,connections_current,bytes_in,bytes_out)
- Static path traversal protection (
.., absolute/empty segments rejected) - Idle keep-alive timeout (default: 10s)
- No external deps (libc + pthreads only)
+---------------------------+
| Linux TCP stack |
+-------------+-------------+
|
SO_REUSEPORT (same IP:port on N sockets)
+---------------------+---------------------+
| | |
+--------v--------+ +--------v--------+ ... +--v-----------+
| Worker Thread 0 | | Worker Thread 1 | | Worker N-1 |
| epoll fd (ET) | | epoll fd (ET) | | epoll fd (ET)|
| listen fd | | listen fd | | listen fd |
| conn table[fd] | | conn table[fd] | | conn table |
+--------+--------+ +--------+--------+ +------+-------+
| | |
+---------- parse + route + response ---------+
|
+------------------------------+------------------------------+
| /healthz | /echo | /static/<path> (sendfile) | /metrics |
+------------------------------+------------------------------+
include/src/coresrc/httpsrc/netsrc/utiltests/
make # release build -> ./httpd
make debug # debug build -> ./httpd-debug./httpd -p 8080 -t 4 -s ./tests/static -i 10Options:
-p <port>: listen port (default8080)-t <threads>: number of event-loop threads (default1)-s <static_root>: static files root (default./static)-i <seconds>: idle timeout for keep-alive connections (default10)
Native Linux:
bash scripts/demo_linux.sh
# or:
make demomacOS via Docker (Linux runtime):
bash scripts/demo_docker.sh
# or:
make demo-dockermake testThis runs:
- C unit tests for HTTP parser (
tests/parser_tests.c) - Python integration test with concurrent traffic (
tests/integration_test.py)
Note: integration tests require Linux because the server runtime uses epoll.
Start server first (release recommended):
make
./httpd -p 8080 -t 4 -s ./tests/static -i 10Automated benchmark script:
make bench
# or:
bash tests/benchmark.shThe script runs wrk --latency, captures p99 for static workload, and enforces:
- static throughput
>= 75k req/s - static p99 latency
<= 10ms
By default it writes tests/benchmark_results_<timestamp>.txt.
Committed reports:
tests/benchmark_results_20260224T231228Z.txt(recorded run)tests/benchmark_results_sample_2026-02-24.txt(tracked sample copy)
wrk -t8 -c256 -d30s http://127.0.0.1:8080/healthz
wrk -t8 -c256 -d30s -s tests/wrk_echo.lua http://127.0.0.1:8080/echoExample tests/wrk_echo.lua:
wrk.method = "POST"
wrk.body = "hello from wrk"
wrk.headers["Content-Length"] = tostring(#wrk.body)hey -n 200000 -c 256 http://127.0.0.1:8080/healthz
hey -n 100000 -c 128 -m POST -d 'hello from hey' http://127.0.0.1:8080/echoFrom tests/benchmark_results_20260224T231228Z.txt:
wrk /healthz:394,523 req/s(p992.12ms)wrk /static/hello.txt:341,518 req/s(p991.73ms)wrk /echo:384,212 req/s(p992.53ms)- benchmark assertions:
assert_static_rps_gte_75000 PASS,assert_static_p99_ms_lte_10 PASS
- Edge-triggered epoll gives high throughput and fewer wakeups, but requires strict drain-until-
EAGAINloops to avoid stalls. - Per-thread listeners with
SO_REUSEPORTremove accept-lock contention, but kernel-level connection distribution can be uneven in some workloads. - Fixed-size per-connection input buffer simplifies parsing and avoids realloc churn, but increases memory use under high connection counts.
- Parser accepts
Content-Lengthbodies and rejects malformed headers early for robustness, but intentionally does not implement chunked request decoding. - Path traversal protection is lexical (
.., absolute paths, empty segments, backslashes) for speed and clarity, but does not attempt symlink canonicalization. - Idle timeout is enforced by periodic scans (1s granularity), which is simple and predictable but less precise than a timer wheel.
- Linux-only implementation (
epoll,sendfile,accept4) - No TLS and no HTTP/2 by design
- No chunked request parsing;
POST /echousesContent-Length