Service: API Service Component: Database Layer Owner: Database Team (@database-team) Last Updated: 2025-10-27 Severity: Usually SEV-2, can escalate to SEV-1
Service: API Service Component: Database Layer Owner: Database Team (@database-team) Last Updated: 2025-10-27 Severity: Usually SEV-2, can escalate to SEV-1
This runbook covers diagnosis and mitigation when the database connection pool is exhausted, preventing the application from acquiring new database connections.
DatabaseConnectionPoolHighDatabaseConnectionPoolExhaustedERROR: Could not acquire connection from pool
ERROR: Timeout waiting for database connection
WARN: Connection pool exhausted, waiting for available connection
ERROR: FATAL: sorry, too many clients already
# Check application metrics
curl http://api.example.com:9090/metrics | grep -E 'connection_pool_(active|idle|max)'
# Expected output showing exhaustion:
# connection_pool_active 100
# connection_pool_idle 0
# connection_pool_max 100
# Check via database directly (PostgreSQL)
psql -h $DB_HOST -U $DB_USER -d $DB_NAME -c "
SELECT count(*) as active_connections,
current_setting('max_connections')::int as max_connections
FROM pg_stat_activity
WHERE state = 'active';
"
# If active_connections >= max_connections * 0.9, pool is near exhaustion
-- PostgreSQL: Find long-running queries
SELECT
pid,
now() - query_start AS duration,
state,
query,
client_addr
FROM pg_stat_activity
WHERE state = 'active'
AND query_start < now() - interval '5 minutes'
ORDER BY duration DESC
LIMIT 20;
-- MySQL: Find long-running queries
SELECT
id,
user,
host,
db,
command,
time,
state,
info
FROM information_schema.processlist
WHERE command != 'Sleep'
AND time > 300 -- 5 minutes
ORDER BY time DESC
LIMIT 20;
# Check application logs for unclosed connections
kubectl logs -l app=api-service --tail=1000 | \
grep -i "connection.*not.*closed\|connection.*leak"
# Check connection acquire/release patterns
# Look for imbalance (more acquires than releases)
kubectl logs -l app=api-service --tail=10000 | \
grep -E "acquired|released" | \
awk '{print $5}' | sort | uniq -c
# Check specific endpoints holding connections
curl http://api.example.com:9090/metrics | \
grep -E 'connection_pool_active.*endpoint'
# Check if traffic spike correlates with pool exhaustion
curl "http://prometheus:9090/api/v1/query?query=rate(http_requests_total[5m])"
# Check for slow queries correlating with pool pressure
curl "http://prometheus:9090/api/v1/query?query=rate(database_query_duration_seconds_sum[5m])"
# Check for specific endpoint causing issues
curl http://api.example.com:9090/metrics | \
grep -E 'http_requests.*duration' | \
awk '{print $1, $2}' | sort -k2 -rn | head -10
When to use: Long-running queries identified holding connections
-- PostgreSQL: Kill specific query by PID
SELECT pg_terminate_backend(12345);
-- Kill all queries running > 5 minutes (be careful!)
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'active'
AND query_start < now() - interval '5 minutes'
AND query NOT LIKE '%pg_stat_activity%' -- Don't kill this query
AND query NOT LIKE '%VACUUM%'; -- Preserve maintenance
-- Verify connections freed
SELECT count(*) FROM pg_stat_activity WHERE state = 'active';
-- MySQL: Kill specific query
KILL 12345;
-- Kill long-running queries
SELECT CONCAT('KILL ', id, ';') AS kill_command
FROM information_schema.processlist
WHERE command != 'Sleep'
AND time > 300
AND user != 'system user'
INTO OUTFILE '/tmp/kill_commands.sql';
SOURCE /tmp/kill_commands.sql;
Expected Result: Connection pool utilization should drop within 30 seconds
When to use: Pool size insufficient for legitimate traffic
# Temporary increase via environment variable
kubectl set env deployment/api-service DB_POOL_SIZE=200
# Or edit config directly
kubectl edit configmap api-config
# Update: DB_POOL_SIZE: "200"
# Restart to apply
kubectl rollout restart deployment/api-service
# Monitor for improvement
watch 'curl -s http://api.example.com:9090/metrics | grep connection_pool_active'
Caution: Don't exceed database max_connections. If app pool size * num_replicas > DB max_connections, you'll create a different problem.
# Check database max_connections
psql -c "SHOW max_connections;"
# Safe pool size calculation:
# app_pool_size * num_app_replicas + buffer < database_max_connections
# Example: 50 * 10 + 100 = 600 < 1000 (safe)
When to use: Connection leak suspected, forcing pool recreation
# Rolling restart to avoid downtime
kubectl rollout restart deployment/api-service
# Monitor restart progress
kubectl rollout status deployment/api-service
# Verify connections cleared
psql -c "SELECT count(*) FROM pg_stat_activity WHERE application_name = 'api-service';"
# Check new pool is healthy
curl http://api.example.com:9090/metrics | grep connection_pool
Expected Result: Connection count should reset to baseline (idle connections only)
When to use: Legitimate traffic spike, need more capacity
# Scale up replicas
kubectl scale deployment/api-service --replicas=20
# Monitor distribution of connections
watch 'kubectl get pods -l app=api-service -o wide'
# Verify traffic distributed
for pod in $(kubectl get pods -l app=api-service -o name); do
echo "$pod: $(kubectl exec $pod -- curl -s localhost:9090/metrics | grep connection_pool_active)"
done
Caution: Ensure total connection pool capacity doesn't exceed database limits
When to use: Specific query pattern identified as slow
-- Identify expensive queries
SELECT
queryid,
calls,
mean_exec_time,
query
FROM pg_stat_statements
ORDER BY mean_exec_time * calls DESC
LIMIT 10;
-- Add index if missing
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
-- Analyze query plan
EXPLAIN ANALYZE
SELECT * FROM users WHERE email = '[email protected]';
When to use: Long-term solution, requires config change
# Deploy PgBouncer in front of database
kubectl apply -f pgbouncer-deployment.yaml
# Update app to connect to PgBouncer instead of direct DB
kubectl set env deployment/api-service \
DB_HOST=pgbouncer.database.svc.cluster.local \
DB_PORT=6432
# PgBouncer pools connections effectively
# App can have large pool, PgBouncer maintains smaller pool to DB
| Condition | Escalate To | Response SLA | |-----------|-------------|--------------| | Pool exhaustion persists > 15 min after mitigation | Database Team (@database-team) | 15 minutes | | Caused by specific query pattern | Team owning that code | 30 minutes | | Database CPU > 80% | Infrastructure Team (@infra) | 15 minutes | | Suspected database failure | On-call Manager | Immediate | | Customer data at risk | Security Team (@security) | Immediate |
# Page database team
pagerduty incident create \
--service database-service \
--title "DB Connection Pool Exhausted" \
--urgency high \
--body "Pool at 100%, app unable to acquire connections. Incident: $INCIDENT_ID"
# Page team owning slow query
# (determine from query pattern)
pagerduty incident create \
--service payments-service \
--title "Slow payment query exhausting DB pool"
When reviewing database-related code:
# Prometheus alerts to add
groups:
- name: database_connection_pool
rules:
- alert: DatabaseConnectionPoolHigh
expr: connection_pool_active / connection_pool_max > 0.85
for: 5m
labels:
severity: warning
annotations:
summary: "Connection pool utilization high"
description: "Pool at {{ $value | humanizePercentage }}"
- alert: DatabaseConnectionPoolExhausted
expr: connection_pool_active / connection_pool_max > 0.95
for: 2m
labels:
severity: critical
annotations:
summary: "Connection pool nearly exhausted"
runbook: "https://runbooks.example.com/db-pool-exhausted"
- alert: DatabaseSlowQueries
expr: rate(database_query_duration_seconds_sum[5m]) > 10
for: 10m
labels:
severity: warning
annotations:
summary: "Database queries running slowly"
# Staging environment test
# 1. Reduce pool size artificially
kubectl set env deployment/api-service-staging DB_POOL_SIZE=5
# 2. Generate load
hey -n 1000 -c 50 https://api-staging.example.com/api/v1/health
# 3. Verify alert fires
# 4. Follow runbook steps
# 5. Verify mitigation works
# 6. Restore normal pool size
# Verify alert fires within expected time
# Verify mitigation reduces pool pressure within expected time
# Check pool status
curl http://api:9090/metrics | grep connection_pool
# Check DB connections
psql -c "SELECT count(*) FROM pg_stat_activity WHERE state='active';"
# Kill long query
psql -c "SELECT pg_terminate_backend(PID);"
# Scale app
kubectl scale deployment/api-service --replicas=N
# Restart app
kubectl rollout restart deployment/api-service
# Increase pool size
kubectl set env deployment/api-service DB_POOL_SIZE=N
-- Active connections by state (PostgreSQL)
SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state;
-- Longest running queries
SELECT pid, now() - query_start as duration, query
FROM pg_stat_activity
WHERE state = 'active'
ORDER BY duration DESC
LIMIT 5;
-- Connections by application
SELECT application_name, count(*)
FROM pg_stat_activity
GROUP BY application_name
ORDER BY count DESC;
-- Connection wait time (if tracked in app)
SELECT percentile_cont(0.95) WITHIN GROUP (ORDER BY wait_time_ms)
FROM connection_pool_wait_times
WHERE timestamp > now() - interval '5 minutes';