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.

Edit this page on GitLab

↑ ↓ to moveEnter to openEsc to close