Skip to content

Anonymous page renders have no cache and no per-address limit — the one core is the bound #179

Description

@Devski

What is there now

Every public page is rendered from the database on every request, and nothing
in front of it counts requests:

  • src/app/[locale]/(public)/[handle]/page.tsx:68 and
    src/app/api/og/[handle]/route.tsx:24 are both force-dynamic. A profile
    view costs 3–7 SQL round trips; a handle-shaped 404 costs 4.
  • The OG route sends public, max-age=86400, s-maxage=86400
    (route.tsx:28), which is advice to a cache that does not exist: there is
    no CDN (Deliver the hero, the assets and user files through a CDN #48), no use cache, and Caddy stores nothing.
  • The rate limit in src/lib/api-route.ts keys on the user id, so it
    cannot apply to an anonymous render at all. deploy/Caddyfile has no
    rate_limit.

The instance is one core with no swap, and dev's PostgreSQL sits on it.

Why now

Found 12.09.2026 in the review of #172, which gave the pool a deadline: the
eleventh caller is now answered with an error after five seconds instead of
queueing for ever. That is the right trade, and it also makes the edge
legible — the failure point of the public surface is now a clean, measurable
boundary rather than a mushy hang. The exposure itself is older than #172 and
unchanged by it: sustained anonymous concurrency against unique handles makes
every visitor, signed in or not, see an error.

Production (#24) will have real accounts behind the same shape.

One more piece, from the same review

src/proxy.ts:89 gives the handle lookup 1.5 s and then abandons it — but
abandoning does not cancel: the query keeps its place in the pool's queue and
may still take a connection for up to five seconds and run for up to ten,
to have its result thrown away. Under load the pool therefore spends capacity
on work nobody is waiting for, and the busier it gets the more of it there is.
Whatever shape this issue takes, those two numbers have to be reconciled — the
middleware's budget and the connection's budget should be one number.

Acceptance criteria

  • A profile page and its OG image are served without touching the
    database on every hit — a cache someone can name, with an invalidation
    story for an owner's edit
  • Anonymous requests are counted somewhere that sees an address; Caddy
    already owns the X-Forwarded-For trust boundary (Login, logout, sessions #8), so it is the
    likely place
  • The middleware's lookup and the connection it takes share one deadline
  • A refused request says so in a way a crawler understands (503 with
    Retry-After, not a 500 that reads as a broken page)
  • Measured, not assumed: the concurrency at which the site degrades today,
    and after the change

Verification

  • Drive concurrent requests at unique handles (/api/og/<random>) and
    record where the error rate turns; repeat afterwards
  • An owner's edit is visible on their public page within the window we
    decided, not after the cache's full life
  • A signed-in user's own pages keep working while the anonymous surface is
    being hammered

Dependencies

Related: #48 (a CDN would carry most of this), #24 (production has the same
shape with real data), #172 (where it was found and what made the edge
visible), #70/#67 (delivery of the assets, the other half of what a visitor
fetches).

Spec: SPEC.md §8, §10 · Size: M · Labels: deployment, enhancement

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    deploymentHow code reaches an environment: pipeline, release procedure, infrastructureenhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions