Skip to content

perf(hosting): cut per-request CPU found by a Postgres load test - #412

Merged
antosubash merged 1 commit into
mainfrom
perf/postgres-loadtest
Oct 8, 2026
Merged

antosubash merged 1 commit into
mainfrom
perf/postgres-loadtest

Conversation

@antosubash

Copy link
Copy Markdown
Owner

What

Load-tested the framework on Postgres (fresh DB with 10k users, 9k role assignments and 100k audit rows; locust AuthedUser mix at 300 users) and profiled the request path with py-spy, cProfile and per-endpoint in-process CPU timing. Full numbers and method: docs/perf/2026-10-08-postgres-loadtest.md.

Headline: one worker is CPU-bound (98% of a core) at ~120 req/s. The p95 of ~10 s is queueing: behind a saturated event loop each request holds its pooled connection far longer than its queries take, so the pool runs dry (QueuePool limit … reached). Four workers with the pool sized under max_connections give 521 req/s, p95 790 ms, 0 failures. That is a deployment lever, already documented in deployment.md.

Fixes (code)

Change Effect
Prefix guard on top-level included routers (_route_guard.py). FastAPI ≥ 0.140 keeps includes as lazy _IncludedRouters with no prefix filter, so every request regex-tested nearly all ~190 routes Route matching was ~30% of a cheap request's CPU. /health 1.98 → 1.46 ms, ~0.5 ms off every request
Guards built at lifespan start FastAPI's lazy per-route dependant build moves off the first request: 360 → 32 ms (startup +~0.35 s)
GZip compresslevel 9 → 5 76 KB page: 6.4 → 2.7 ms of event-loop CPU for 1.2% more bytes
SetupMiddleware verdict refresh is single-flight; an expired complete verdict answers while it refreshes Stops a stampede of has_administrator checkouts every 5 s under load
Trivial sync dependency getters → async def (permissions, feature_flags, file_storage, tenants, users) No threadpool hop per request
/admin/users status cards: one COUNT(*) FILTER scan instead of 3 round-trips

The guard is a pure optimisation: it answers Match.NONE only when the path lacks the common literal prefix of the router's effective paths. It re-derives that prefix whenever FastAPI's route version changes, and is a no-op on a FastAPI without _IncludedRouter. Tests check that routing is unchanged with the guard on (params, 404, 422, slash redirect, add_route after guarding) and that the skip actually happens; the skip test fails without the guard.

Deliberately not changed (written up in the perf doc)

Verification

  • Full suite on SQLite: 3814 passed, 11 skipped
  • Users, tenants and the new tests on Postgres (SM_TEST_DATABASE_URL): 668 passed
  • make lint: green
  • Load test before/after: table in the perf doc

https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4

Load-tested the framework on Postgres (10k users, 100k audit rows, locust
300 users) and profiled the request path. A single worker is CPU-bound at
~120 req/s; the fixes below remove overhead the profile pinned:

- Route matching: FastAPI >= 0.140 keeps includes as lazy _IncludedRouter
  placeholders with no prefix filter, so each request regex-tested nearly
  all ~190 routes (~30% of a cheap request's CPU). A prefix guard per
  top-level include, derived from its effective paths and re-derived on
  route-version change, skips subtrees that cannot match. /health
  1.98 -> 1.46 ms. Built at lifespan start, it also moves FastAPI's lazy
  per-route dependant build off the first request (360 -> 32 ms).
- GZip at level 5 instead of Starlette's 9: ~1% larger, 2.4x less CPU.
- SetupMiddleware refreshes its verdict single-flight and serves the
  expired complete verdict while refreshing, instead of every in-flight
  request checking out a pooled connection when the TTL lapses.
- Trivial sync dependency getters are now async (no threadpool hop).
- /admin/users status counts: one COUNT(*) FILTER scan, not three.

Findings and numbers, including what was deliberately left alone
(pool_pre_ping, the tenant/soft-delete statement filter, audit COUNT),
are in docs/perf/2026-10-08-postgres-loadtest.md.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review ✅ Completed 2026-10-08T13:00:50.537037Z b86abc5 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying simple-module-python with  Cloudflare Pages  Cloudflare Pages

Latest commit: b86abc5
Status: ✅  Deploy successful!
Preview URL: https://263358bd.simple-module-python.pages.dev
Branch Preview URL: https://perf-postgres-loadtest.simple-module-python.pages.dev

View logs

@antosubash
antosubash merged commit 7db61a0 into main Oct 8, 2026
14 checks passed
@antosubash
antosubash deleted the perf/postgres-loadtest branch October 8, 2026 17:42
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