Stage 7 · Master
Phase 19 — Production Deployment
Autoscaling
Scale gateway on CPU because CPU is genuinely its bottleneck, scale organization-api on its connection-pool saturation ratio because CPU is not — and cap maxReplicas against the shared Postgres budget from Performance Engineering so autoscaling cannot recreate the exact incident it exists to prevent.
The Right Autoscaling Metric Depends on the Actual Bottleneck, Not a Default
gateway is stateless request routing and JWT verification — CPU-bound work that scales predictably with request rate, making HorizontalPodAutoscaler's default CPU-utilization target a correct fit. organization-api is different: Performance Engineering's connection-pooling incident and the load-testing lesson's 140-VU knee both traced its actual bottleneck to pgxpool connection acquisition, not CPU — a replica can sit at 30% CPU while every request queues on Acquire. Scaling organization-api on CPU would leave it exactly as exposed to pool exhaustion as before Lesson 3's fix; it needs to scale on the same pool-acquired ratio that diagnosed the original incident.
API Gateway already ships a CPU-target HPA, written when the gateway became the platform's single public entry point. That manifest is correct and is not repeated here — CPU utilization genuinely is gateway's bottleneck signal. The question this lesson answers is what to do for a service where the default signal is wrong.
| Service | Scaling signal | Why | Ceiling set by |
|---|---|---|---|
| gateway | CPU utilization, 65% | Stateless routing and signature verification scale with request rate | Node capacity |
| organization-api | Pool acquired/max ratio, 0.70 | Replicas can sit at 30% CPU while every request queues on Acquire | The shared PostgreSQL connection budget |
prometheus-adapter Turns a PromQL Expression Into Something an HPA Can Target
The pgxmetrics ratio used to diagnose the original incident — hoa_organization_postgres_pool_acquired_connections / hoa_organization_postgres_pool_max_connections — already exists in Prometheus. prometheus-adapter exposes it to the Kubernetes custom-metrics API under a name an HPA can reference directly, so no new instrumentation is needed in organization-api itself; the metric that diagnosed the incident is the same metric that now prevents it.
rules:
custom:
- seriesQuery: 'hoa_organization_postgres_pool_acquired_connections{namespace!="",pod!=""}'
resources: { overrides: { namespace: { resource: "namespace" }, pod: { resource: "pod" } } }
name: { as: "organization_postgres_pool_acquired_ratio" }
metricsQuery: |
avg(hoa_organization_postgres_pool_acquired_connections{<<.LabelMatchers>>})
/
avg(hoa_organization_postgres_pool_max_connections{<<.LabelMatchers>>})This rule does not create a new metric in organization-api — it teaches prometheus-adapter to compute the existing incident-diagnosis ratio on demand and serve it through the custom-metrics API the HPA controller queries.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: organization
namespace: {{ .Release.Namespace }}
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: organization }
minReplicas: 2
maxReplicas: 6 # capped against the shared Postgres budget — see the failure analysis below
metrics:
- type: Pods
pods:
metric: { name: organization_postgres_pool_acquired_ratio }
target: { type: AverageValue, averageValue: "700m" }
behavior:
scaleUp:
stabilizationWindowSeconds: 15
policies: [{ type: Pods, value: 2, periodSeconds: 60 }]
scaleDown:
stabilizationWindowSeconds: 300
policies: [{ type: Pods, value: 1, periodSeconds: 180 }]The 0.70 target is not a round-number guess — the load-testing lesson found the pgxmetrics ratio crossing 0.9 at the exact moment p95 latency broke its SLO at the 140-VU knee. Triggering scale-up at 0.70 gives the platform room to add capacity before the ratio reaches the value already known to correlate with SLO breach, and the 15-second stabilization window (shorter than gateway's 30) reflects that pool exhaustion compounds faster than CPU pressure once it starts.
maxReplicas: 6 Is Not Arbitrary — a Higher Number Silently Reintroduces the Original Incident
The first draft of this HPA set maxReplicas: 20, matching organization's Deployment's own resource headroom with no reference to anything outside the service. Lesson 3 fixed the pool-exhaustion incident by capping organization at MaxConns: 3 per replica specifically because the shared PostgreSQL instance has a 97-connection real budget split across eleven services — that arithmetic did not go away just because scaling is now automatic. At maxReplicas: 20, an HPA doing exactly what it is supposed to do during a real traffic spike would drive organization alone to 60 connections, more than half the platform's entire shared budget, degrading — or reintroducing pool exhaustion for — every other service sharing the same Postgres instance.
| maxReplicas | organization's connection contribution (× MaxConns: 3) | Outcome under sustained load |
|---|---|---|
| 20 (first draft) | 60 | Reproduces Lesson 3's incident platform-wide — organization alone can exhaust more than half the shared 97-connection budget |
| 6 (shipped) | 18 | Matches the per-service allocation the shared-budget formula already assumed when a service occasionally scales beyond its typical 2–3 replicas |
Every replica an HPA can create is a multiplier on that replica's own resource consumption elsewhere in the platform — most concretely, its database connection budget. Setting maxReplicas without re-checking the shared-budget arithmetic from Performance Engineering does not remove the ceiling; it just moves the incident from 'the config file said 20 MaxConns' to 'the autoscaler quietly created enough replicas to blow the budget on its own'.
Re-run the Load Test That Found the Knee — With Autoscaling Live
A successful run shows organization's replica count rising from 2 toward — but not past — 6 as the k6 ramp crosses the previous knee, the pool-acquired ratio staying below the 0.70 target throughout instead of pegging at 1.0, and the p95 threshold that failed in the load-testing lesson's original run now passing across the full VU range. Autoscaling did not move the platform's real ceiling; it added capacity fast enough, and within a budget-aware limit, to keep the same fixed ceiling from being reached during the same load pattern that previously reached it.
Applied exercise
Size auth's HPA against its own share of the shared connection budget
services/auth-service verifies a JWT on every gateway-routed request and, from the connection-pooling lesson, already runs 3 replicas with a service-specific MaxConns override higher than organization's, due to its higher request volume. Product wants an HPA on auth for an upcoming traffic event.
- State auth's current per-replica MaxConns value and its total connection contribution at 3 replicas, using the connection-pooling lesson's numbers.
- Propose a maxReplicas value for auth's HPA, showing the arithmetic against the shared 97-connection budget and accounting for organization's own maxReplicas: 6 already consuming part of that budget.
- Decide whether auth should scale on CPU or on the same pool-acquired-ratio custom metric as organization, and justify the choice from auth's actual bottleneck rather than copying organization's pattern by default.
- Write the resulting HorizontalPodAutoscaler manifest.
Deliverable
A justified maxReplicas value with the shared-budget arithmetic shown, a justified metric choice, and a complete HPA manifest.
Completion checks
- The maxReplicas figure is derived from the shared 97-connection budget across all services' current allocations, not chosen independently of organization's.
- The metric choice (CPU vs. custom pool ratio) is justified by auth's actual bottleneck, not assumed from organization's example.
- The manifest includes scaleUp/scaleDown behavior tuned to avoid flapping, matching the asymmetric stabilization-window pattern from this lesson.
Why does organization-api's HPA target the pgxmetrics pool-acquired ratio instead of CPU utilization?
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.