Cut per-request origin work: batched article loads, no session write per page

The origin was 3–8 ms per page, almost all of it per-statement overhead:
an issue page ran two statements per pick (~55 on a 25-article issue) and
every signed-in page wrote its session row back because `take_flash`
called `Session::remove`, which marks the session modified even when the
key is absent.

- `Db::get_articles` loads an issue's articles and their social rows in
  two statements; both branches of `web::issue::load` use it. The
  single-id and batch queries share one projection via a macro.
- `take_flash` reads before removing, and touches a signed-in session at
  most once a day so the inactivity expiry still slides. Anonymous
  requests never create a session.
- `Server-Timing: app;dur=<ms>` on every response, outermost layer.
- `reject_early_data`: 425 for a non-safe method that arrived as TLS 0-RTT
  data, so nginx `ssl_early_data on` is safe (RFC 8470 §5.2).
- `[profile.release]`: fat LTO, one codegen unit (binary 46 → 29 MB).

Docs: the Cloudflare proxy was retired on 2026-09-05 after measuring
+43 ms per signed-in page from Boston; README reverse-proxy section is
now the direct setup (upstream keepalive, 0-RTT lines) and the CDN
runbook carries a retired-status banner.

Dev seed, app-side: `/` 21 → 5 statements, 3.7 → 1.0 ms; `/feed.xml`
44 → 12, 9.1 → 3.0 ms; session writes per signed-in page 1 → 0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Va5eMEmWEnjMXBsBob5FDW
This commit is contained in:
2026-09-05 01:34:39 +00:00
co-authored by Claude Fable 5.1
parent bc40773964
commit e727d53b85
8 changed files with 353 additions and 27 deletions
+14
View File
@@ -1,5 +1,19 @@
# Runbook — putting daily.hallada.net behind Cloudflare
> **Status (2026-09-05): retired.** The `daily` record is **DNS only** again, one
> day after this rollout. Measured from Boston with a session cookie (so the edge
> bypassed its cache, as it does for every signed-in request): 70 ms after the
> TLS handshake through the proxy against 27 ms straight to the origin. The edge
> adds a proxy hop and its own overhead on every uncached request, and at this
> traffic the cache is cold for anonymous readers too, so the proxy cost the one
> signed-in reader more than it gave anyone. The zone stays on Cloudflare's
> nameservers; flipping the record back to Proxied re-enables everything below.
> When you do, also put the `cloudflare-real-ip.conf` include back into the nginx
> server block (README, *Reverse proxy*) — it was removed with the proxy because
> a direct connection from a Cloudflare address could otherwise name its own
> client IP. The origin's `Cache-Control` matrix (§0) is still in force and is
> what makes the toggle safe in either direction.
**Written:** 2026-09-04 for the production host. Steps 13 happen in the Cloudflare
and registrar dashboards; steps 45 are on the server as an operator with `sudo`.
The origin-side changes (the `Cache-Control` matrix below) ship in the same