Rung 5 — EPOLL HTTP/1.1 Echo Server 🟧 HARD GATE #1

Headline. A single-threaded, event-loop HTTP/1.1 server in C, using epoll on Linux, sustaining 100k+ RPS on a 4-core laptop for keepalive echo requests — with a benchmark methodology, graphs, and a 24-hour leak-free soak test to back the number.

Month target. M9 (March 2027). HARD GATE: publicly shipped by the last day of M9. Miss this and invoke the reset protocol in 13_discipline/.


What You Build

Repo name: epoll-echo or nhttp (pick one and commit). Single-binary, Linux-first. Optional io_uring variant behind a build flag.

Mandatory functional scope:

  1. Single-threaded event loop built on epoll with edge-triggered mode. No thread pool. No fork-per-connection. The point of the rung is that one thread pushed correctly beats naive threading.

  2. Non-blocking sockets end-to-end. Every accept, read, write returns EAGAIN correctly and the loop handles it.

  3. HTTP/1.1 parser (hand-rolled or a vetted small parser like llhttp or picohttpparser). Supports GET; parses Content-Length and Transfer-Encoding: chunked for POST; supports Connection: keep-alive (the default in 1.1) and Connection: close.

  4. Keepalive by default; pipelining supported (multiple requests on one connection).

  5. Per-connection state machine with explicit states (READING_REQUEST, WRITING_RESPONSE, CLOSING); no implicit state hidden in call stacks.

  6. Configurable via CLI flags: port, worker count (must accept 1 even if you later add SO_REUSEPORT for scaling experiments), max connections, timeout.

  7. Graceful shutdown on SIGINT: stop accepting, drain in-flight requests, close cleanly.

  8. Zero fd leaks verifiable by lsof -p <pid> before and after a 10k-request burst.

Optional stretch: io_uring backend behind -DUSE_IO_URING, side-by-side benchmark.

Mandatory infrastructure:

  • Benchmark harness using wrk (or wrk2 for latency accuracy). Script bench/run.sh that reproduces the headline number on a documented machine.

  • Benchmark graphs committed to docs/: RPS vs. concurrency, p50/p99 latency vs. RPS, RPS vs. connection count. Generate with a simple gnuplot or matplotlib script.

  • 24-hour soak testbench/soak.sh runs wrk at moderate load for 24h. RSS memory sampled every 5 minutes and plotted. The plot must be flat (± 5% jitter is fine; a monotonic climb is a leak and it fails the rung).

  • ThreadSanitizer run even though it’s single-threaded — you’ll add SO_REUSEPORT or a small worker pool eventually; get in the habit.

  • README with: pitch, benchmark graph inline, exact machine spec (CPU, kernel, NIC, sysctl tunings), “how to reproduce in 10 minutes.”

  • Blog post — 1500-2500 words, with graphs, explaining how you got from 5k RPS to 100k RPS. The optimization journey is more valuable than the final number.

Target size: ~<phone_number_or_numberic_id_or_random_id_6> LOC of C.


Why This Rung, Why Now — and Why It Is a HARD GATE

Rung 5 is the first artifact in the ladder that a systems-role hiring manager will treat as a real work sample. Everything before it — the toolchain journal, the container library, the shell — is a signal about your habits. Rung 5 is a signal about your engineering ceiling. When you tell a Cloudflare / Fastly / Neon / Redis-adjacent study partner that you understand event loops, they will ask how. The answer “I built one that sustained 100k RPS, here’s the repo and the flame graph” is a career-defining sentence.

It is a HARD GATE because M10 and M11 are Rung 6’s territory, and Rung 6 depends on the engineering-under-pressure muscle you build here — profiling, measuring, iterating, refusing to accept a number you can’t reproduce. If Rung 5 slips into M10, Rung 6 slips into M12, and Rung 7 dies. The ladder collapses from the bottom.


Acceptance Criteria (all mandatory — this is a hard gate)

  • Sustains ≥ 100,000 RPS for keepalive echo on a 4-core laptop, reproducible from bench/run.sh, with machine spec documented

  • p99 latency under 5ms at the headline RPS

  • TSan clean under a 60-second load run

  • ASan clean under a 30-second load run (ASan is slow; use a smaller run)

  • HTTP/1.1 spec conformant for GET — tested with curl, httpie, and one adversarial client that sends malformed requests (server rejects, does not crash)

  • kill -9 on the server leaves no dangling client fds on the client machine (reset properly)

  • 24-hour soak test result committed — memory plot is flat, no leaked fds via /proc/<pid>/fd count sampled

  • Benchmark graphs committed to docs/ — RPS-vs-conn, latency-vs-RPS, RPS-vs-CPU

  • Blog post published and linked from README

  • Submitted to Show HN (with the blog post, not just the repo)


Where to Publish

  • GitHub: pinned on profile. Topics: c, epoll, http-server, high-performance, networking.

  • Personal blog: the optimization-journey post is the star. This is the post you point recruiters at.

  • Hacker News — Show HN: submit the blog post. Title format: “Show HN: I built an epoll HTTP server in C that does 100k RPS on a laptop”. Only submit if the benchmark is real and the code will hold up to comments — HN will pick it apart.

  • Reddit — r/programming: cross-post the blog after the HN attempt.

  • Reddit — r/C_Programming: post a smaller technical-detail thread, e.g., “How I halved my p99 latency by fixing edge-triggered epoll starvation”.

  • LinkedIn: long-form post targeting Bangalore/India systems engineers and recruiters. Include the graph as an image.


Signal to Recruiter / Employer

“This person can build the concurrency substrate of a real service. They know what a spurious wakeup is, they know why edge-triggered epoll starves without draining, they can defend a benchmark under adversarial questioning. They are hireable at Cloudflare / Fastly / Neon / a serious networking or systems team.”

Rung 5 is the rung that changes the shape of your inbox. Recruiters who ignored Rungs 1-4 start reaching out after Rung 5 lands on HN or LinkedIn. Prepare a short elevator answer for “what did you learn?” — you will be asked.


Common Failure Modes

  1. The benchmark is wrong. You benchmark on localhost, with all cores available to wrk, with kernel TCP tuning that isn’t documented. Your 100k number is real on your box and unreproducible everywhere else. Detection: run the benchmark on a different Linux machine (AWS t3.medium, a friend’s laptop) and confirm the graph replicates. Document the delta.

  2. Silent failure under load. accept returns EMFILE, your loop logs nothing, throughput degrades. Detection: every syscall error path must have a counter incremented and dumped on SIGUSR1.

  3. Edge-triggered starvation. You read once per EPOLLIN event; a fast client fills the socket faster than you drain and you get “phantom” backlog. Detection: a benchmark scenario where clients send bursts of 100 pipelined requests per connection — if throughput collapses, you’re starving.

  4. The 24h soak faked. You run for 45 minutes, extrapolate, and call it 24h. Detection: the memory plot must have 288 data points (5min × 288 = 24h). A reviewer will count them.

  5. HTTP parser is a toy. You accept any bytes as a request. curl works, real-world traffic doesn’t. Detection: run the server against a wrk script that sends 1% malformed requests — it must reject them, not crash.



Estimated Hours

  • Event loop skeleton + non-blocking sockets: 12h

  • HTTP/1.1 parser integration + state machine: 16h

  • Keepalive + pipelining: 10h

  • Benchmarking + iteration to hit 100k: 30h (this is where most of the time goes — profiling, perf, flame graphs)

  • 24h soak infrastructure + running it: 6h (the running is passive; the plumbing is active)

  • Blog post + graphs + polish: 15h

  • Optional io_uring variant: 15h (only if the above is done)

Total: ~90-105 hours across M8-M9. ~13h/week for 8 weeks. This is the peak weekly load of the year; plan accordingly.


Prior-Art / Inspirations to Study First

  • redis/src/ae.c — Redis’s event-loop abstraction. Small, elegant, production-tested. Read it end-to-end before you start.

  • libuv/src/unix/ — particularly stream.c and core.c. Overkill for your project but shows the industrial version.

  • Marek Majkowski’s Cloudflare blog posts on epoll edge cases — search “cloudflare blog epoll”. The “epoll is fundamentally broken” post is required reading before you commit to a design.

  • h2o/picohttpparser — the parser you’ll most likely integrate. Read picohttpparser.c; it’s ~500 LOC of masterful C.

  • Julia Evans’ “How does epoll work?” post — for the mental model.


Return to README.md · Previous: 04_rung_4_mini_shell.md · Next: 06_rung_6_simd_gemm_benchmark.md