Skip to main content

Sold Out, Not Priced Out: What Hetzner's "Limited Availability" Means When Your Autoscaler Can't Buy a Server

13 min readDora NodaDora Noda
Share

Hetzner told you prices were going up. The status page has been saying something harder to budget: some servers are sold out.

On April 1, 2026, Hetzner raised cloud 30–37% and dedicated up to 50%, with memory add-ons up ~575% — a pass-through of DRAM up 171% YoY and 250–300% on spot RAM. On June 15, CCX/CPX dedicated lines rose another 113–176% for new orders while CX/CAX shared lines rose 30–33%. Most teams recomputed their bill.

The quieter line on status.hetzner.com reads Limited availability of Cloud plans (incident aa5ce33b) — not a price, a capacity wall. For a CAPH fleet that provisions its whole pool on one provider, that notice is a failure mode distinct from insufficient funds. A MachineDeployment scale-up doesn't fail because it is too expensive. It fails because the API has nothing to sell you in that location and type right now.

This post shows what that failure looks like on CAPH, the multi-location fallback that survives it, and why owning the queue beats inheriting a hosted PaaS's invisible one — while being honest that no single-provider fleet escapes the hardware squeeze underneath.

The price shock was the headline — the capacity wall is the new failure mode

The 2026 Hetzner pricing story already has a paper trail:

AdjustmentDateScopeScale
Round 1Feb 19 announced, Apr 1 effectiveCloud servers (DE/FI), dedicated, object storage, volumesCloud +30–37%, dedicated up to +50%, volumes/object storage +30–53%, memory add-ons ~+575%
Round 2Jun 15 effective (new orders/rescales)CPX/CCX dedicated-vCPU vs CX/CAX shared-vCPUCPX/CCX +113–176% (e.g., CCX13 €15.99 → €42.99, +169%; CCX63 €374.49 → €853.49), CX/CAX +30–33%
Root causeAll of 2026DRAM, NAND, NVMe procurementTrendForce/DRAM spot +171% YoY; Hetzner: "extremely high procurement costs for new hardware"; OVH founder warned of +5–10% cloud hikes on the same squeeze

A three-node CX22→CX23 pool that was €30/mo is now ~€40, a CCX pool that was €48 is now €129, and the cheap-CPX-for-production pattern is gone after June 15.

Capacity is not a second hike. It is a third state your autoscaler must handle. Hetzner's Cloud API has long returned no capacity in this location as a distinct error from quota or billing — visible in runner tooling that documents Retry if: Resource is not available (Limited availability of Cloud plans) with create_wait of 360×10 s (one hour) before giving up. That retry exists because the condition is transient but not self-healing on one try.

What that means for CAPH:

  • Price: the HCloudMachine reconciler never fires — the autoscaler doesn't try because budgets say no.
  • Capacity: the reconciler fires, Hetzner returns availability failure for that server_type × location, and the Machine sits Provisioning with FailedCreate while pending pods stay unscheduled. The node group exists but never becomes Ready.

With one MachineDeployment at hel1 + cpx22, that single retry is your whole HA plan.

The core deliverable: three scale-up scenarios and the YAML that survives them

The same workload — three nodes scaling to five under a deploy spike — through three placement plans on post-June-15 DE/FI list prices and real locations (nbg1, fsn1, hel1). Intra-Hetzner backbone latency, not Internet egress.

ScenarioNode pool shapeScale requestWhat happens on "limited availability"Monthly cost delta vs planOperator action
A — Single pool, single type/zoneMachineDeployment: cpx22 in hel1 (AMD shared, 4 vCPU/8 GB)+2 nodesBoth HCloudMachines return Limited availability of Cloud plans in hel1; autoscaler backs off; pods pending 10–60 min until Hetzner restocks€0 until success, then +€69.48 if restock at old price, or +€142 at post-June price if rescale crossed Jun 15Manual: edit template to nbg1, wait for reconcile, or add new MD
B — Two pools, same type, two locationsMachineDeployments: cpx22 in hel1 + cpx22 in nbg1, priority expander hel1:10, nbg1:5+2 nodesAutoscaler tries hel1 first; on failure, priority expander falls back to nbg1 within next scale loop (typically 30–60 s)Same per-node price (€34.74/mo cpx22 shared), +€0 fallback tax; cross-location pod traffic stays on Hetzner's private network (included)None — expander handles it
C — Two pools, two architecturescpx22 (x86, nbg1) + cax11 (Arm64 Ampere, fsn1)+2 nodescpx22 sold out in nbg1; fallback to cax11 Arm pool succeeds (CAX line rose only ~30% vs +113–176% for CCX/CPX, and has had better availability in H1 2026)cax11 €5.18/mo vs cpx22 €34.74/mo — fallback is cheaper but requires linux/arm64 images (buildpacks already emit both)Ensure images are multi-arch; no template edit

June 15 split the catalog: the fallback you want (CAX Arm) is the line Hetzner raised least, because Ampere supply is less contended than AMD EPYC. A fleet that tolerates Arm for stateless nodes gets a cost and capacity win.

The before and after is one field and one extra object. Before — the brittle pool that breaks:

yaml
# BEFORE — one location, one server type, one point of failure
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: HCloudMachineTemplate
metadata:
  name: workers-cpx22-hel1
  namespace: caph-system
spec:
  template:
    spec:
      type: cpx22
      imageName: ubuntu-24.04
      location: hel1
---
apiVersion: cluster.x-k8s.io/v1beta1
kind: MachineDeployment
metadata:
  name: workers
  namespace: caph-system
  annotations:
    cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size: "3"
    cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size: "10"
spec:
  clusterName: prod
  replicas: 3
  selector:
    matchLabels: { pool: workers }
  template:
    spec:
      clusterName: prod
      version: v1.31.4
      infrastructureRef:
        apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
        kind: HCloudMachineTemplate
        name: workers-cpx22-hel1

After — two pools with priority expander, no human edit. Note location not datacenter — CAPH v1.0.7+ no longer validates server_type, and datacenter is deprecated in favor of location (June 2026 also renamed to CPX Gen2/CX Gen3/CAX — hard-coded server_type strings are the other brittle assumption):

yaml
# AFTER — two pools; autoscaler picks the one with capacity
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: HCloudMachineTemplate
metadata:
  name: workers-cpx22-nbg1
  namespace: caph-system
spec:
  template:
    spec:
      type: cpx22          # AMD shared — post-June price, but x86 fallback
      imageName: ubuntu-24.04
      location: nbg1
---
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: HCloudMachineTemplate
metadata:
  name: workers-cax11-fsn1
  namespace: caph-system
spec:
  template:
    spec:
      type: cax11          # Ampere Arm — cheapest + best availability in 2026
      imageName: ubuntu-24.04
      location: fsn1
---
apiVersion: cluster.x-k8s.io/v1beta1
kind: MachineDeployment
metadata:
  name: workers-cpx-nbg1
  namespace: caph-system
  annotations:
    cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size: "2"
    cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size: "10"
spec:
  clusterName: prod
  replicas: 2
  selector: { matchLabels: { pool: workers-cpx } }
  template:
    spec:
      clusterName: prod
      version: v1.31.4
      infrastructureRef:
        kind: HCloudMachineTemplate
        name: workers-cpx22-nbg1
---
apiVersion: cluster.x-k8s.io/v1beta1
kind: MachineDeployment
metadata:
  name: workers-cax-fsn1
  namespace: caph-system
  annotations:
    cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size: "1"
    cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size: "10"
spec:
  clusterName: prod
  replicas: 1
  selector: { matchLabels: { pool: workers-cax } }
  template:
    spec:
      clusterName: prod
      version: v1.31.4
      infrastructureRef:
        kind: HCloudMachineTemplate
        name: workers-cax11-fsn1

Cluster-autoscaler config that makes the fallback automatic (the priority expander, not the default random):

yaml
# cluster-autoscaler args / ConfigMap snippet — priority expander
expander: priority
# priority-expander ConfigMap — higher value = tried first
data:
  priorities: |-
    10:
      - caph-system/workers-cpx-nbg1
    5:
      - caph-system/workers-cax-fsn1

With this, Limited availability of Cloud plans in nbg1 for cpx22 does not leave pods pending for an hour while a 360-retry loop spins. The autoscaler tries the next priority node group on its next scale loop and lands the Arm node in fsn1 instead.

What the failure looks like on the wire

When capacity is the constraint, the signals differ from quota or budget. On stock CAPH + autoscaler:

  • kubectl get hcloudmachines -n caph-system — new HCloudMachine shows Ready: False with Limited availability of Cloud plans or no capacity in this location in Conditions.
  • kubectl get machines -n caph-systemMachine stuck Provisioning with FailedCreate for the HCloudMachine.
  • kubectl get pods --all-namespaces — workload pods Pending; autoscaler logs show unable to scale node group or pod didn't trigger scale-up, not Insufficient cpu.
  • kubectl logs -n kube-system deployment/cluster-autoscaler loops ScaleUp failed: ... Limited availability at scale-up-delay.

Two CAPH gotchas:

  • spec.datacenter is gone. Manifests that still set datacenter: nbg1-dc3 send POST /servers with a field the API now 410 Gones — the June 2026 removal flagged in the changelog. Use location: nbg1.
  • CAPH v1.0.7+ no longer validates server_type. A typo or deprecated type (Oct 2025 server_types deprecated wave that introduced Gen2/Gen3) no longer fails at admission — it fails late at provision, same shape as a capacity miss. Check HCloudMachineTemplate.status.capacity (from ListServerTypes) as pre-flight, not the error after 360 retries.

The autoscaler defaults that survive sold-out inventory

Five settings. Fallback is a system, not a patch.

1. Separate MDs per location × type — never one pool. One HCloudMachineTemplate per (location, server_type) and one MachineDeployment per template. Two to three MDs across nbg1/fsn1/hel1 covers EU without touching US ash (only pricier CPX/CCX after June 15).

2. Autoscaler annotations with headroom. Beyond min-size/max-size, set replicas with headroom:

text
desired = ceil( peak_pods / pods_per_node ) + headroom_nodes
headroom_nodes = ceil(0.2 * desired)  # 20% — one extra per five
min-size = desired_floor
max-size = desired_floor + burst

One spare cax11 at €5.18/mo beats a 10-minute pending-pod incident. Scaling exactly to demand is cheapest — and fails first on a wall.

3. Priority expander, not random. Stock random/least-waste spreads evenly — good for bin-packing, bad for availability. priority gives a deterministic order: cheapest x86 first, Arm fallback next, US last. Without it, the autoscaler can retry the sold-out pool three times before trying the next.

4. Respect HCloudMachineTemplate.status.capacity. CAPH sets status.capacity via ListServerTypes once per template; the autoscaler reads it for scale-from-zero. If you kept a stale cpx21 string after the Gen2/Gen3 rename, capacity is zero and the MD is silently skipped — same symptom as sold-out. Audit types quarterly; prefer cax where Arm works, cpx/ccx only where dedicated vCPU is required.

5. Warm node, not longer retries. create_wait: 360 (one hour of 10 s retries on one tuple) optimizes for eventual success. A priority fleet optimizes for fast success elsewhere. Keep CAPH retries short (30–60 s) and let the expander fail over. One-hour retries belong to hobby clusters, not production pools.

Why owning the fleet still beats borrowing someone else's queue — and where it doesn't

Price hides the capacity question. Same shortage, two control planes:

QuestionHosted PaaS (shared infra)Self-hosted CAPH fleet
Who sees "Limited availability"?The operator, not you. Your deploy sits queued; the status page says degraded provisioning later.You. Machine Events, CAPH conditions, autoscaler logs — all in your cluster.
Who picks fallback?The PaaS. You chose a region at creation; they chose the type. If it is sold out, you wait.You — priority expander and kubectl apply on MDs, not a ticket.
Who sets headroom?The PaaS, bin-packing across tenants. Headroom for you is utilization loss for them — so it runs tight.You. One cax11 at €5/mo warm is a per-pool decision.
What does the DRAM squeeze hit?Both. The 30–50% hike and AX102 €124→€454 dedicated spike also tightened the capacity your PaaS rents upstream — you just don't see it until deploys stall.Both. Owning machines doesn't create DRAM. But the failure is visible and routable, not latency with no knobs.

Self-hosting is not outage-free. H1 2026 saw 30,246 outages across 1,082 providers — owned fleets had theirs. The difference is narrower: a single-tenant fleet's failures are uncorrelated with others' bad deploys and observable without waiting for a vendor queue to drain — a queue that took Scaleway 51 days region-by-region for object storage in the same period.

The honest caveat from the TODO: "own the machines" still rides the same chain that caused the squeeze. If cpx22 is sold out in hel1, AI HBM demand took the DRAM that would have built it, and the backlog hits every EU metal vendor. Multi-location inside Hetzner helps until the shortage is provider-wide — then the fallback is a second provider (Hetzner + Netcup RS1000 G12 or a small Vultr CAPI pool), not a third location on the same provider. That hop is an architecture decision to scope before you need it, not during OVH's 5-day H1 2026 hosting outage window.

Audit your fleet today — 5 checks

Run against your management cluster before the next scale-up, not after.

1. List every location and server type you depend on.

bash
kubectl get hcloudmachinetemplates -A -o json \
  | jq -r '.items[] | "\(.metadata.namespace)/\(.metadata.name)  type=\(.spec.template.spec.type)  loc=\(.spec.template.spec.location)"' \
  | sort
# FAIL if you see one line total. PASS if you see ≥2 locations and ≥2 types.

2. Prove you are not still on datacenter.

bash
kubectl get hcloudmachines,hetznermachines -A -o json | jq -e '
  [.items[] | select(.spec.type? or .spec.datacenter?)] | length == 0
' && echo "PASS: no datacenter field" || echo "FAIL: still naming datacenters"
# Also grep your git repo: rg -n "datacenter:" infra/

3. Prove each template's type still exists in that location.

bash
kubectl get hcloudmachinetemplates -A -o json \
  | jq -r '.items[] | "\(.metadata.name)  capacity=\(.status.capacity // "unknown")  type=\(.spec.template.spec.type)"'
# Any "unknown" or 0 capacity means the catalog renamed that type — update it.
# Cross-check: hcloud server-type list | grep -E "cpx|cax|ccx|cx"

4. Prove the autoscaler can actually fail over.

bash
kubectl get machinedeployments -A -o json \
  | jq -r '.items[] | "\(.metadata.name)  min=\(.metadata.annotations."cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size")  max=\(.metadata.annotations."cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size")  replicas=\(.spec.replicas)"'
# FAIL if any MD lacks both annotations, or if max == replicas (no burst room).
 
kubectl get configmap -n kube-system cluster-autoscaler-priority-expander -o yaml 2>/dev/null \
  | grep -A20 priorities || echo "FAIL: priority expander ConfigMap missing — fallback is random, not ordered"

5. Price and reserve headroom explicitly.

bash
# Pick a pool, compute headroom cost for one warm node — do it with real numbers, not "negligible":
# cax11 fsn1 €5.18/mo, cpx22 ~€34.74/mo shared (post-June DE/FI), ccx13 €42.99/mo
# One spare cax11 = one coffee. One pending deploy = one incident.
kubectl scale machinedeployment/workers-cax-fsn1 -n caph-system --replicas=2  # hold +1 warm
# Record: echo "headroom: 1×cax11 fsn1 €5.18/mo as of 2026-06-15" >> infra/capacity-budget.md

If all five pass, capacity is a normal operational constraint — priced, visible, and routable — not a surprise that turns scale-up into a silent queue. If any fail, the Limited availability notice is a preview of your next Machine that never becomes Ready.


The DRAM/NAND squeeze that doubled memory costs and drove two Hetzner hikes in one quarter is not a Hetzner story alone. OVH, Netcup, and every EU metal vendor cites the same root, and the same shortage limits capacity a hosted PaaS can rent upstream. Self-hosting doesn't escape that market. It escapes waiting behind other tenants for it to restock, with no knob to turn.

A fleet that spreads across locations, survives a type rename, pays €5/mo for one warm Arm node, and fails over in 30 seconds didn't solve the shortage. It stopped pretending one (type, location) was a plan.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, with a Render-compatible API and an MCP server any agent can call. The fleet above is not a thought experiment: it is a MachineDeployment you operate. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex