Managing Connection Limits in Cloud Run and Cloud Functions
This guide is part of Serverless Function Connection Management. Cloud Run differs from Lambda in one respect that changes every calculation: an instance can serve many requests concurrently. That restores the case for a pool larger than one, makes the arithmetic two-dimensional, and introduces a failure that Lambda does not have — a pool sized for one request while the instance is serving eighty.
The signature is a service that works at low traffic and degrades sharply at a threshold, with max-instances never reached:
Error: timeout exceeded when trying to connect
at /workspace/node_modules/pg-pool/index.js:45:11
# Cloud Run metrics: instance count 3 of 100, container concurrency 80/80
# Cloud SQL: 12 connections, instance at 8% CPU
Three instances, eighty concurrent requests each, and twelve connections total. The pool is the bottleneck and both the platform and the database look idle.
Rapid Incident Diagnosis
1. Read the concurrency setting. gcloud run services describe <svc> --format='value(spec.template.spec.containerConcurrency)'. The default is 80, which is very different from Lambda’s effective 1 and is the number the pool must serve.
2. Compare pool size to concurrency. A pool of 1 or 2 on an instance with concurrency 80 will queue almost immediately. This is the most common cause and the cheapest to check.
3. Compute the ceiling. max-instances × pool_size is the maximum backend demand. Compare it to the service’s share of Cloud SQL’s max_connections.
4. Confirm what the connector is doing. The Cloud SQL connector and Auth Proxy authenticate and encrypt; neither pools. Backend count equals client connection count regardless of which is used.
The Arithmetic
Two constraints, both of which must hold.
pool_size ≥ concurrent_queries_per_instance (from Little's Law, per instance)
pool_size ≤ service_allowance ÷ max_instances (from the budget)
The first uses the instance’s own concurrency, not the request concurrency directly. An instance serving 80 concurrent requests, each holding a connection for 8 ms out of a 120 ms response, has 80 × (8/120) ≈ 5.3 concurrent queries — so a pool of 7 or 8 covers it, not 80. This is the async advantage described in Connection Pool Sizing Formulas, and on Cloud Run it is what makes high concurrency affordable.
The second is the familiar division. With a 200-connection allowance and max-instances: 100, the ceiling is 2 — below what the first constraint needs, and the signal that max-instances is too high for a direct connection.
# Bound the instance count so the two constraints can both be satisfied.
gcloud run deploy orders-api \
--concurrency=80 \
--max-instances=25 \ # 200 allowance ÷ 25 = 8 per instance
--min-instances=1 \ # keeps one warm; holds pool_min connections
--set-env-vars=DB_POOL_MAX=8,DB_POOL_MIN=2,DB_ACQUIRE_TIMEOUT_MS=2000
max-instances is the lever with no downside other than throttling: requests beyond the capacity of 25 instances queue at the platform rather than failing at the database, which is a far better failure mode and affects only this service.
Cloud Functions and the Concurrency Difference
Cloud Functions (2nd gen) runs on Cloud Run and inherits the concurrency setting; 1st gen is fixed at one request per instance and behaves like Lambda. Checking which generation is deployed is worth doing before applying either model, because the correct pool size differs by an order of magnitude.
| Platform | Requests per Instance | Correct Pool Size | Dominant Multiplier |
|---|---|---|---|
| Cloud Functions 1st gen | 1 | 1 | Instance count |
| Cloud Functions 2nd gen | configurable, default 1 | concurrency-derived | Instance count |
| Cloud Run, default | 80 | 5–15 typically | Instance count |
| Cloud Run, concurrency 1 | 1 | 1 | Instance count |
| Cloud Run + PgBouncer | 80 | 5–15 | Proxy pool size |
The last row is the durable answer at scale. Cloud SQL has no managed equivalent of RDS Proxy, so a pooler has to be run — typically PgBouncer as a shared Cloud Run service or on GKE, sized independently of the application’s instance count. Running it as a sidecar in each instance collapses nothing, since each sidecar serves only its own instance.
Connector, Auth Proxy, or Direct
All three connection paths open one backend per client connection. The choice is about authentication and network topology, not about connection count — which is the single most common misconception on this platform.
The connector library is the modern default. It handles IAM authentication and mTLS in-process, needs no sidecar, and adds no hop. Its max-instances interaction is nil.
The Auth Proxy does the same as a separate process. Its --max-connections flag caps how many client connections it accepts, which makes it a useful circuit breaker in front of the database — but only when it is a shared deployment; as a sidecar the flag applies per instance and the product is unchanged.
Direct connection over private IP avoids both, at the cost of managing credentials yourself. It is the lowest-latency path and the one with the least operational surface, and it is entirely reasonable when the service runs inside a VPC with Secret Manager for credentials.
Common Failure Patterns & Remediation
| Symptom | Root Cause | Exact Fix | Validation |
|---|---|---|---|
| Timeouts with instance count far below max | Pool smaller than per-instance concurrency | Size the pool from concurrency × hold-time ratio | Local queueing stops; instance count rises normally |
too many connections as instances scale |
max-instances × pool exceeds the allowance |
Bound max-instances; add a shared pooler |
Backends flat as instances scale |
| Pooler deployed, backend count unchanged | Pooler running as a sidecar | Move it to a shared service | Backends fall to the pooler’s pool size |
| Cold-start latency includes a handshake | Client constructed per request | Construct at module or package scope | First-request latency falls after warm-up |
| Backends persist after traffic stops | Instances retained with min-instances |
Expected — account for it in the floor | Trough count matches min-instances × pool_min |
| Connections lost after a quiet period | Idle reaping between requests | Retry once on a connection error | Errors absent across quiet cycles |
Validation and Verification
Run a deliberate load test to max-instances before traffic does it for you.
# Drive enough load to reach the instance ceiling.
hey -z 3m -c 400 https://orders-api-xxxx.a.run.app/health
# In parallel, watch both sides:
gcloud run services describe orders-api \
--format='value(status.traffic)' # instance count via Cloud Monitoring
-- On Cloud SQL, confirm the total stays inside the allowance.
SELECT count(*) AS backends
FROM pg_stat_activity
WHERE application_name = 'orders-api' AND backend_type = 'client backend';
Two conditions must hold: the backend count settles at or below the derived allowance, and no request fails with a connection error. If instance count reaches its maximum and requests queue at the platform instead, that is the intended behaviour — throttling is a much better failure mode than exhausting a shared database.
Frequently Asked Questions
Should containerConcurrency be lowered to reduce connections?
Does min-instances hold connections continuously?
min-instances × pool_min. That is usually a small number and worth it for the cold-start reduction, but it should appear in the budget rather than being a surprise.Is the Cloud SQL connector slower than a direct connection?
What is the equivalent of RDS Proxy on GCP?
How does this apply to Cloud Functions 1st gen?
Does Cloud Run’s CPU throttling between requests affect connections?
--cpu-boost and --no-cpu-throttling — background timers in the connection pool do not run reliably between requests, which reproduces the Lambda freeze problem on a platform that otherwise avoids it. Enabling always-allocated CPU restores normal pool timer behaviour at a billing cost.Should the pool be warmed at container start?
pool_min connections before reporting ready keeps the first requests off the handshake path and means a container that cannot reach the database never receives traffic.Related
- Serverless Function Connection Management — the parent guide on the execution-environment model.
- Reusing Database Connections Across Lambda Invocations — the single-request-per-instance case.
- GCP Cloud SQL Connection Pooling — the connector and Auth Proxy in detail.
- Configuring Cloud SQL Auth Proxy Connection Limits — what the proxy’s connection flag actually bounds.