Sizing
The reference root's inputs that set the size of each service and of the database, with their defaults.
The reference root ships with defaults that suit a production environment of moderate size. This page lists the inputs that change the size, so that you can make dev and test smaller and prod bigger.
Database#
| Input | Default | Notes |
|---|---|---|
db_tier |
db-custom-2-7680 (2 vCPU, 7.5 GB) |
The Cloud SQL machine type |
db_availability_type |
REGIONAL |
A standby in a second zone, with automatic failover. ZONAL halves the instance cost and suits dev and test |
cloud_sql_edition |
ENTERPRISE |
|
enable_read_replica, read_replica_tier |
false, db-g1-small |
A read replica for read-heavy pages |
db_pool_size, db_max_overflow |
4, 4 |
Connections per worker. Multiply by workers and instances, and keep the total under the database's max_connections |
Services#
| Service | Memory and CPU | Instances (min–max) | Root inputs |
|---|---|---|---|
| backend | 8Gi, 2 vCPU |
1–8 in prod, 1–4 elsewhere | backend_memory (1Gi to 8Gi), backend_min_instances, backend_max_concurrency (default 10 requests per instance) |
| compiler | 24Gi, 8 vCPU |
0–10 | compiler_memory, compiler_cpu, compiler_max_instances |
| scheduler | module default | 1–10 (1–3 in dev) | — |
| input-logs | module default | 1–10 (1–3 in dev) | — |
| runner (data-quality executor) | module default | up to 5 in prod, 3 elsewhere | — |
| jobs | 2Gi, 1 vCPU |
1–3 | — |
| frontend | module default | 0–10 | — |
The compiler's memory and CPU go together. Cloud Run needs at least 2 vCPU above 4Gi, 4 above 8Gi, 6 above 16Gi and 8 above 24Gi, and the root checks that before it plans.
Small and medium installations#
This page gives no sizes per installation size (assets monitored, concurrent users): none has been measured on Cloud Run. The only measurement is the local one below, one backend process on a laptop. The defaults above are the reference root's, not the result of a load test.
Start from the defaults in prod and lower them in dev and test. Then watch the Memory Pressure and Cloud SQL connection saturation alerts (see Monitoring and alerts), and size from what they show.
Local measurement (2026-10-13)#
A first run of the two API load tests (perf/k6/, k6 2.3.0) against a stack on a laptop, not on Cloud Run. It shows
what one backend process does and where it stops. It is not production sizing.
Conditions:
- The backend as a single uvicorn process (one event loop, no extra workers), the scheduler beside it, and PostgreSQL 16 on the same machine, all run from the source tree with the demo seed.
- A 10-core laptop with 16 GB of memory, shared with other work (load average 4 to 12 during the runs).
- The per-user rate limiter off, as the load-test script sets it: every virtual user is the same dev identity, and with the limiter on a run measures its 429 responses (from about 33 requests a second) rather than the stack.
- The database pool at the default outside prod, 5 connections plus 3 overflow, unless the row says otherwise.
- A virtual user runs one pass of six requests, then waits one second: far busier than a person at the UI.
| Scenario | Knee: requests a second (users) | p(95) at the knee | Whole run: requests a second, failed, p(95) | Thresholds |
|---|---|---|---|---|
api-read (second of two matching runs) |
48 (10 users) | 22 ms | 7.5, 8.7 %, 30.2 s | missed: failures, p(95), checks |
api-mixed |
84 (17 users) | 14 ms | 19.8, 2.8 %, 207 ms (reads 204 ms) | missed: failures, checks |
api-read, pool 10 + 5 |
79 (17 users) | 61 ms | 18.6, 2.0 %, 112 ms | missed: failures, checks |
The stack misses the budget (fewer than 1 % failed requests, p(95) under 1 s, checks above 99 %) on all three runs.
In api-mixed the p(95) threshold covers the reads only (http_req_duration{kind:read}); the acknowledge and reopen
writes count towards the failures and checks. Up to the knee, the p(95) of all requests in each 20-second window stayed
at or under 61 ms. Past it, between 13 and 23 users, the backend's event loop stops for 10 seconds to almost two
minutes at a time, requests time out after 10 seconds with a 503, and it recovers in between.
The first resource to run out is the database connection pool, and the reason is the event loop, not the database.
Catalog search (GET /api/v1/search) runs its queries on the event loop. When the pool is momentarily full, the
search waits there for a connection, up to 2 seconds for each of its queries in turn, and nothing else on the
loop moves. The requests that hold the connections cannot finish while the loop waits, so the pool stays full until
the waits time out. During the stalls the connections sat idle in a transaction, no query was running in Postgres,
and the backend process peaked at 14 % to 60 % of one CPU core depending on the run. The prod pool size (10 plus 5)
moved the knee from 10 to about 17 users but did not change the failure. This is a defect in the search route, not
a capacity limit: the numbers above will move once it is fixed.