Safe scaling defaults for Cloud Run + Postgres
Cloud Run autoscaling is not a database strategy. Safe defaults keep the application from scaling itself into a Postgres incident before the team understands the workload.
Cloud Run can add application instances faster than Postgres can become a different database.
Safe defaults make the application reach an intentional limit before the database reaches an accidental one. They’re conservative until production evidence earns more concurrency, more instances, or larger pools.
Start from the connection budget
Reserve database sessions for administration, migrations, monitoring, replicas or poolers, and every other application. Give the Cloud Run service only its share.
Then satisfy the basic upper bound:
max service instances × maximum DB sessions per instance
≤ service connection budgetInclude overlapping revisions during deployment and any worker processes that create separate pools. A pool max of four inside three processes is a per-instance claim of twelve.
This article applies the budget. Why Cloud Run + Postgres needs a connection budget defines it.
Tune the knobs as one contract
The important settings interact:
- Container concurrency controls how much request work one instance accepts
- Maximum instances bounds fleet width
- Pool maximum bounds database sessions per process or instance
- Acquisition timeout bounds how long work waits for a session
- Statement and transaction timeouts bound database occupation
- Request timeout bounds the synchronous response path
Higher concurrency can reduce instance count for light requests, but it can overload one pool when most requests are database-heavy. Lower concurrency can cause more instances to start, multiplying pools. Neither direction is safe without the fleet-wide math.
Begin with measured request behavior, not Cloud Run’s maximum or a framework default.
A conservative starting posture
For a small internal API, a reasonable first profile might look like this:
cloud_run:
concurrency: 10
max_instances: 5
request_timeout_seconds: 30
postgres:
pool_max_per_instance: 4
pool_min_per_instance: 0
acquire_timeout_ms: 1000
statement_timeout_ms: 5000Those numbers aren’t a recommendation detached from database size. They express a posture: no unbounded fleet, no eager minimum pool on every instance, no long invisible wait for a scarce session, and no assumption that request work may hold the database indefinitely.
Calculate the actual budget before copying any value. A five-instance service with a four-session pool can claim twenty sessions, plus rollout overlap if both revisions are live.
Prefer small pools
Most web services don’t need a session for every concurrent request. Many requests perform no database work. Others borrow a connection for a short query and return it.
A small pool creates local backpressure and keeps the database from carrying hundreds of mostly idle backends. Increase it only when pool wait time is material while Postgres still has CPU, I/O, locks, and connection headroom.
Set pool minimum to zero or a very small number for elastic services unless warm sessions solve a measured latency issue. Multiplying idle minimums across scaled instances is an efficient way to reserve capacity for work that doesn’t exist.
Bound waiting before Postgres becomes the queue
When every pool connection is busy, a request should wait briefly or fail with a controlled response. It shouldn’t occupy an application worker for most of the Cloud Run timeout while additional instances repeat the same demand.
Short acquisition deadlines expose saturation near the caller. Pair them with bounded retries and load shedding. For durable work, enqueue rather than retrying synchronously until one request becomes several.
A visible 503 or 429 at the intended boundary is healthier than a fleet of requests waiting on a database that can no longer complete work.
Keep transactions narrower than requests
Acquire a session after validation and release it before external HTTP calls, file processing, rendering, or other non-database work. Transactions should cover the consistency boundary, not the handler’s entire narrative arc.
Cloud Run may keep processing after a request timeout, so database work must use its own deadlines and cancellation. The transport timeout isn’t a transaction manager.
Separate service shapes
APIs, queue consumers, jobs, migrations, and admin tools shouldn’t inherit one pool profile.
A worker processing database-heavy messages may need concurrency one or two and a tiny pool. A read-heavy API can tolerate higher HTTP concurrency with the same small pool. A migration needs a dedicated execution window and reserved headroom, not the production API’s credentials and scale policy.
Give each workload an application_name, service account, pool configuration, and connection budget that makes its pressure visible.
Loosen only from evidence
Run production-shaped load tests including traffic bursts and revision overlap. Watch:
- instance count and startup rate
- pool utilization and acquisition latency
- active, idle, and waiting Postgres sessions
- transaction age and lock waits
- query latency, CPU, memory, and I/O
- request errors and tail latency
Raise concurrency when instances are underused and database demand stays bounded. Raise max instances when useful throughput is limited before Postgres pressure appears. Raise pool size only when session scarcity, rather than database work, is the bottleneck.
Aim for a predictable failure mode: a bounded queue, a classified rejection, or an asynchronous handoff. Autoscaling is useful only while it respects the finite system behind it.
More in this domain: Infrastructure
Browse allCloud Tasks vs. Pub/Sub for Cloud Run: routing, retries, and rate limits
Use Cloud Tasks for a specific operation that needs scheduling or controlled dispatch. Use Pub/Sub when independent consumers need the same event. Either HTTP handler must tolerate retries.
Cloud Run scale to zero: cold starts and minimum instances
Keep scale to zero when the first request can wait. Pay for minimum instances when measured startup latency hurts the service, and decide background CPU separately.
Direct VPC egress vs. Serverless VPC Access for Cloud Run
Use Direct VPC egress for most new Cloud Run connections to a VPC. Check address capacity, startup behavior, and outbound routing before retiring a working connector.
Internal-only Cloud Run: ingress, identity, and the caller’s network path
An internal Cloud Run service needs an allowed request path and an authorized caller. The correct setup differs for Cloud Tasks, another Cloud Run service, and human access.
Pulumi stack config: values, code, and secrets
Stack config supplies values within an infrastructure design. Keep topology and operating policy explicit in code, validate the inputs, and manage secrets according to who owns their lifecycle.
Related patterns
How we decide between Cloud SQL connectors, Auth Proxy, and private IP
Cloud SQL connectors, the Auth Proxy, and private IP are not interchangeable secure connection options. They change identity, routing, deployment shape, and how much network plumbing the team actually owns.
GKE Autopilot as the escape hatch from Cloud Run
When Cloud Run stops fitting, the next move is usually GKE Autopilot: more Kubernetes-shaped control without immediately taking on the full burden of Standard clusters.
Why we default to Cloud Run for SME internal platforms
For SME internal platforms, Cloud Run is our default because it covers a large share of useful workload shapes without forcing teams to own cluster operations before they have earned that surface area.
Cloud Run request timeouts don't kill your code (so your architecture has to)
A Cloud Run request timeout ends the request, not necessarily the work. If the operation can outlive its caller, the system needs explicit job semantics instead of hope.