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 allHow 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.
IAM DB auth for Cloud SQL: when it simplifies security and when it complicates delivery
IAM DB auth can reduce password sprawl and make revocation cleaner, but it also turns database access into an identity operating model that depends on disciplined service-account boundaries.
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.
Cloud Run scaling from zero is a feature until it isn't
Scale to zero is a good default for request-driven services, until startup delay, warm-capacity needs, or instance caps turn it into user-visible reliability behavior instead of a pricing feature.
Direct VPC egress vs Serverless VPC Access for Cloud Run: our default
We default to Direct VPC egress for Cloud Run because it is the cleaner networking shape: fewer moving parts, no connector resource, and costs that scale with the service instead of beside it.
Related patterns
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.
"Internal-only" Cloud Run isn't just a checkbox
Making a Cloud Run service private is not one toggle. It is a decision about ingress, routing, caller path, and IAM working together as one access model.
How we diagnose and fix a "too many connections" incident for Cloud Run + Postgres
A "too many connections" incident is rarely a one-line fix. It usually exposes a bad contract between Cloud Run scaling, app pool behavior, and database capacity.