You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 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).
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:68andsrc/app/api/og/[handle]/route.tsx:24are bothforce-dynamic. A profileview costs 3–7 SQL round trips; a handle-shaped 404 costs 4.
public, max-age=86400, s-maxage=86400(
route.tsx:28), which is advice to a cache that does not exist: there isno CDN (Deliver the hero, the assets and user files through a CDN #48), no
use cache, and Caddy stores nothing.src/lib/api-route.tskeys on the user id, so itcannot apply to an anonymous render at all.
deploy/Caddyfilehas norate_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:89gives the handle lookup 1.5 s and then abandons it — butabandoning 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
database on every hit — a cache someone can name, with an invalidation
story for an owner's edit
already owns the
X-Forwarded-Fortrust boundary (Login, logout, sessions #8), so it is thelikely place
Retry-After, not a 500 that reads as a broken page)and after the change
Verification
/api/og/<random>) andrecord where the error rate turns; repeat afterwards
decided, not after the cache's full life
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