Skip to content

Zero-copy static files: fd-backed response bodies over sendfile(2) - #104

Merged
codetalcott merged 2 commits into
mainfrom
claude/sendfile-static
Aug 25, 2026
Merged

Zero-copy static files: fd-backed response bodies over sendfile(2)#104
codetalcott merged 2 commits into
mainfrom
claude/sendfile-static

Conversation

@codetalcott

Copy link
Copy Markdown
Owner

Completes the roadmap's recorded zero-copy step, on top of the sendfile(2) binding from #102.

StaticFiles read every byte of every hit into memory and copied it through the encode buffer, so a 64 MB file cost 64 MB of RSS per concurrent response. The bytes now never enter the process:

RSS growth serving 3×64 MB
this PR 64 KB
buffered (sabotage) 200,784 KB

The shape

HTTPResponse gains an fd-backed body beside body_raw — the two are alternatives, so encode_into writes only the head and the loop owns the transfer.

Both write paths learned it, which was not optional. The event loop pumps the file across readiness events; the blocking listen_and_serve loop does it in a plain loop. A response kind only one path understood would be a trap for anyone embedding the simple server, and it would present as headers promising a body that never arrives.

Ownership has exactly one home. The descriptor moves to the ConnectionProvision the moment the head is encoded, and close_body_fd is the only release — reached from the completion, from _close_slot (so a client vanishing mid-transfer cannot leak it), and from HEAD stripping. That is why StaticFiles opens the file last, after every refusal has already returned.

Two details worth flagging in review:

  • _pump_body_fd advances the offset before branching on the result. Darwin reports a short write as -1/EAGAIN with a positive count; treating -1 as "nothing happened" resends those bytes.
  • A sendfile error closes the connection rather than continuing. The head is already on the wire promising Content-Length bytes, so a short body is indistinguishable from a truncated response — closing is at least an error the client can detect.

The ETag change, stated deliberately

The validator was a wyhash64 over the whole file, which a path that never reads the bytes cannot compute. It is now derived from size and mtime, as nginx and Apache do.

The trade: a rewrite preserving both size and mtime is now a cache hit the content hash would have caught. What did not change: what the tag claims. It is still weak, so If-Range remains unsatisfiable and a conditional range still falls back to the full representation. The reasoning in static.mojo's docstring — which previously justified that from the content hash — has been rewritten rather than left to quietly describe code that no longer exists.

Verification

poe smoke-sendfile (new): byte-exact 64 MB delivery three times over, the RSS assertion, a mid-file Range, a bodyless HEAD that keeps the real Content-Length, and an ETag round-trip in both directions (its own → 304, another file's → 200).

Verified load-bearing: buffering the body again takes RSS growth from 64 KB to 200,784 KB, and the guard names the cause rather than just failing.

test_static.mojo now reads the fd body with pread, so its 28 assertions are about content rather than about which field happens to hold it — better coverage than before, since they now exercise the offset the Range path actually uses.

All 25 smoke rows green (including smoke-notes, which covers static type/ETag/304 and two traversal probes over a real socket); build-all + test-all clean; warning ratchet at 68.

🤖 Generated with Claude Code

codetalcott and others added 2 commits August 25, 2026 09:53
`StaticFiles` read every byte of every hit into memory and then copied it
through the encode buffer, so serving a 64 MB file cost 64 MB of RSS per
concurrent response. The bytes now never enter the process at all:
measured at 64 KB of RSS growth while serving 192 MB, against ~200 MB
for the same files buffered.

`HTTPResponse` gains an fd-backed body beside `body_raw` — the two are
alternatives, and `encode_into` therefore writes only the head. BOTH write
paths learned to transfer it, which is the part that was not optional: the
event loop pumps it across readiness events, and the blocking
`listen_and_serve` loop does it in a plain loop. A response kind that only
one path understood would be a trap for anyone embedding the simple
server, and it would present as headers promising a body that never
arrives.

Ownership is the delicate part, so it has exactly one home. The
descriptor moves to the `ConnectionProvision` the moment the head is
encoded, and `close_body_fd` is the only thing that releases it —
reached from the completion, from `_close_slot` (so a client that
vanishes mid-transfer cannot leak it), and from HEAD stripping. Because
of that, `StaticFiles` opens the file LAST, after every refusal has
already returned.

`_pump_body_fd` loops rather than sending once per readiness event, or a
large file would cost one loop pass per socket buffer, and it advances
the offset BEFORE branching on the result: Darwin reports a short write
as -1/EAGAIN with a positive count, so a caller that treats -1 as "nothing
happened" resends those bytes.

A sendfile error closes the connection instead of continuing. The head is
already on the wire promising Content-Length bytes, so a short body is
indistinguishable from a truncated response — closing is at least an error
the client can detect.

The ETag changed with it, deliberately, and the docs now say so where the
old reasoning lived. It was a wyhash64 over the whole file, which a path
that never reads the bytes cannot compute; it is now derived from size and
mtime, as nginx and Apache do. The trade: a rewrite preserving both is a
cache hit the content hash would have caught. What did NOT change is what
the tag claims — still weak, so `If-Range` stays unsatisfiable and a
conditional range still falls back to the full representation.

`poe smoke-sendfile` is the new guard: byte-exact 64 MB delivery three
times over, the RSS assertion, a mid-file Range, a bodyless HEAD that
keeps the real Content-Length, and an ETag round-trip in both directions.
Verified load-bearing — buffering the body again takes RSS growth from
64 KB to 200,784 KB and the guard names the cause. `test_static.mojo`
reads the fd body with `pread`, so its 28 assertions are about content
rather than about which field holds it.

All 25 smoke rows green; build-all + test-all clean; ratchet at 68.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The task existed and passed locally, but it was not in the workflow, so
PR #104's green tick did not include it. A guard CI never runs is a guard
that will rot silently -- and this one is the whole evidence for the
zero-copy claim.

Both platforms matter here rather than one: sendfile(2) takes its first
two arguments in the opposite order on Darwin and Linux and reports a
short write differently, so the implementation has a comptime branch of
which a macOS-only run exercises exactly half.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codetalcott
codetalcott merged commit d8ca012 into main Aug 25, 2026
4 checks passed
@codetalcott
codetalcott deleted the claude/sendfile-static branch August 25, 2026 14:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant