Size a cluster for cost
Running your own nodes on Hetzner is already far cheaper than a hyperscaler (a large cloud like AWS, Azure, or Google Cloud). Sizing them well captures the rest of the saving. The method is one idea: match each part of your load to the cheapest machine that fits it.
Hetzner Cloud and bare metal run the same Syself Linux node image and join a cluster the same way. The cost decision comes down to how each backend bills, not any difference in what the node runs.
The cost model: bare metal vs cloud#
The two differ in what you get and how you pay. A bare-metal server gives you dedicated CPU cores, local NVMe storage, and a dedicated network link with unlimited traffic, all for a flat monthly price. On cloud you rent by the hour, dedicated vCPU costs extra, and both bandwidth and traffic are capped. For steady load, bare metal usually lands around 60% cheaper than the equivalent cloud servers.
| Bare metal | Cloud | |
|---|---|---|
| Billing shape | Flat monthly per server you own | Per VM, per hour it runs |
| CPU | Dedicated cores, included | Shared by default; dedicated vCPU costs extra |
| Best for | Steady, always-on load | Elastic, bursty, unpredictable load |
| Storage | Local NVMe included, survives reprovision | Network volumes, billed per GB |
| Network | Dedicated 1G, upgradeable to 10G, unlimited traffic | Around 300 to 500 Mbps, 20 TB included, then per TB |
| Getting one | Minutes to add from your pool, hours for a new server to be delivered | Ready in minutes, scale to zero when idle |
Bare metal gives you the most compute per euro for load that runs all the time, plus the storage and network to run databases on it. Cloud gives you servers in minutes and the freedom to pay nothing for capacity you are not using. The main catch with bare metal: a new server takes a few hours to be delivered, so plan a little headroom instead of buying reactively.
Fixed baseline plus elastic burst#
The most cost-effective layout for most teams is a bare-metal baseline for the load that never goes away, plus a cloud pool on the autoscaler for the peaks. The autoscaler adds cloud nodes when pods need them and removes them when idle.
workers:
machineDeployments:
- class: workeramd64baremetal
name: md-base
replicas: 3
- class: workeramd64hcloud
name: md-burst
metadata:
annotations:
cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size: "0"
cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size: "10"
You pay a flat rate for the base and pay for cloud only during spikes. See Mix cloud and bare-metal pools for how to steer workloads across the two.
Local NVMe vs network storage#
Bare-metal local NVMe (fast disks attached straight to the server) carries no per-gigabyte network charge and no network hop, and it survives a reprovision. That makes it the cheap, fast home for databases and other stateful services.
Use cloud network volumes only when you need storage that detaches from one node and reattaches to another. Put stateful workloads on the bare-metal base, for both cost and performance.
Match the machine line to the load#
Hetzner Cloud has three server lines, and each fits a different load:
- CPX (regular, shared vCPU): the balanced default.
cpx32(4 vCPU, 8 GB RAM) is the starting worker type. For memory-heavy workloads, step up tocpx41(8 vCPU, 16 GB) orcpx51(16 vCPU, 32 GB) before you add more, smaller nodes. - CCX (dedicated vCPU): for steady, latency-sensitive load. Use CCX, or bare metal, for control-plane nodes (the nodes running the Kubernetes API and etcd) and any service that needs consistent CPU.
- CX (cost-optimized, shared vCPU): the cheapest line, on older hardware with variable performance and limited availability. Good for a spiky, tolerant burst pool on the autoscaler, but never for control planes or latency-sensitive services.
Own the bare-metal servers for reserved, always-on load: a machine you keep busy is cheaper owned than rented.
GPU workloads run on bare metal only, on Hetzner's GEX line or an auction server with a supported NVIDIA card. There is no HCloud GPU type, so a GPU pool cannot burst to cloud the way a CPU pool can.
For the machine types you can pick from, see Server types and sizing .
Control-plane nodes run kube-apiserver, etcd, and the scheduler, never your workload pods (they carry a NoSchedule taint). Do not size them for application load, only for cluster scale.
cpx32 (the default) leaves roughly 4 to 5 GB free for Kubernetes components after the OS and reservations. That is enough for most clusters. For clusters with more than 20 nodes, or heavy API traffic, move up to cpx41 (16 GB) or larger. See Server types and sizing for the full guidance.
Right-size against allocatable, not the spec#
A node's schedulable capacity is less than its raw size. kubectl describe node shows two numbers: Capacity (the raw hardware) and Allocatable (what the scheduler may actually hand to pods). Allocatable is always lower, because:
Allocatable = Capacity - systemReserved - evictionHard
The kubelet (the Kubernetes agent on each node) sets aside systemReserved for the OS and system daemons, and reserves further headroom (evictionHard) so a pod burst cannot starve the node. Size pools against allocatable, not the spec sheet. Otherwise you pay for headroom you cannot schedule into, or you pack pods until the kubelet starts evicting them.
The reserved slice grows with node size, but shrinks as a percentage of total RAM. See Node resources and limits for the reservation numbers by node size.
A cluster-wide pod cap also bounds how many workloads fit per node. Syself sets maxPods: 220, and the kubelet refuses the 221st pod. On a small node, you run out of memory long before you hit that cap. For right-sizing, allocatable memory is almost always the binding constraint, not the pod count.
Tip
Plan workload capacity a little under 220 pods per node. A few slots go to per-node system pods that Syself runs on every node.
See the Node resources and limits reference for the eviction thresholds too.
Feed the allocatable numbers, not the spec-sheet numbers, into the comparison below.
Work the comparison#
Hetzner prices change, so do the arithmetic with current numbers, not a figure baked into a doc.
Price the steady load as owned bare metal #
Take your steady CPU and memory demand. Price it as owned bare-metal servers, at their flat monthly rate.
Price the burst as hourly cloud #
Take your peak-minus-steady demand. Price it as cloud VMs that run only for the hours you need them.
Compare against all-cloud #
Add the two. Compare the total against the single elastic-cloud bill you would pay to serve the whole peak on rented VMs all month.
Pull the per-machine prices from the Hetzner Cloud pricing page and the Robot server list. The saving comes from two places: a flat, low rate for the base instead of a metered hyperscaler rate, and nothing paid for burst capacity between spikes.
Once the layout is set, wire the burst pool to the cluster autoscaler so it adds and removes cloud nodes for you. The same method applies pool by pool if you dedicate capacity per client , and the business case for the switch is in Cloud cost reduction .