Concepts
Last updated
Hetscale is a control plane, not a platform: it reads metrics and calls the Hetzner Cloud API in your project. Your traffic goes from your load balancer to your servers and never passes through Hetscale.
Node and node group
A node is one Hetzner Cloud server. A node group is a set of identical, stateless nodes behind one load balancer that scale together within a minimum and a maximum. Most setups have one group, for example “web”. Every plan allows as many groups as you need; a plan names the number of nodes they run together in Live or paused, the servers you already run included.
In the panel, a project is one Hetzner project with the token that reads it, and its node groups are on its Groups tab: Projects → the project → Groups. Its token is replaced in place with Replace token (Replace a project’s token), and node groups and projects are archived, never deleted (Archive and restore).
Seed server
The existing server you point Hetscale at joins the group as an adopted, protected member. It counts toward capacity, but scale-in never selects it and healing never replaces it; if it becomes unhealthy, Hetscale alerts and starts a managed node beside it. The group’s template is taken from it. Promotion to a normal managed member is an explicit action with a confirmation: a server deleted by scale-in loses its legacy price. A seed Coolify deploys is never offered for promotion, because it is the only server Coolify knows. Your other servers behind the load balancer join the same way, and Hand your own servers over to the node group retires them one at a time.
Template and release
The template is what a new node is made of: server type, locations, network, firewall, SSH
keys and labels. The release is what it runs: a snapshot id or an image digest. Each group
holds a current release pointer; every scale-out and every healing action boots from it, never
from :latest. Your CI can move the pointer; for the snapshot adapter, a new snapshot of the seed
is the next release, also when Coolify deploys the seed. Nodes on an older release keep running
it until you replace them: Replace older servers on the group’s Servers page starts one from
current, waits for its health check and drains one older node, one at a time. To go back, make an
earlier release current on the Deployment page:
Releases, rollouts and rolling back.
Servers without public addresses
New servers take the seed’s public network: a seed without a public IPv4, IPv6 or both gives servers without them, and no switch turns the addresses off for the copies of a seed that has them. A server without any public address is created without firewalls, because Hetzner applies Cloud Firewalls only to a public interface, and joins the load balancer through its private IP, the balancer and the server sharing the seed’s network. A server without a public IPv4 takes no Primary IPv4, and Hetzner charges it no IPv4. Each public IPv4 or IPv6 address counts toward the account’s Primary IP limit (10 for a new account, load balancers included), and a server without any public address takes none.
A server without any public address reaches the internet only through your own NAT server: a route
0.0.0.0/0 in the network to the NAT server and, in the server’s own operating system, a default
route to the network’s gateway and a resolver, set in files the copies boot with. Hetzner’s tutorial
How to set up NAT gateway for private Cloud Networks
shows both. On a copy’s first boot, cloud-init writes its own network file, 10-netplan-*.network
on Ubuntu, and systemd-networkd applies the first file that matches by name, so the route’s file
must sort before it, as the tutorial’s 10-enp7s0.network does. A route set by hand on the seed
does not reach the copies. The preflight of the first release warns when none of the seed’s
networks has a route 0.0.0.0/0 (Go Live), but the route’s file
is yours to check: the Rehearsal’s health check passes without a way out.
Allowlists and new servers
Every server Hetscale creates with a public IPv4 gets whatever address Hetzner assigns it, so no allowlist can know the address in advance. A database, a payment provider or a partner API that admits only listed addresses refuses the new server. The server then fails its health check if the check uses that service, or serves errors if it does not. There are three ways to keep new servers admitted.
- The private network. When the database runs in a Hetzner network your seed is attached to, allow that network’s address range on the database. New servers join the networks the seed was in when the group’s first release was taken, so every one of them is inside the range. So is any other server you attach to that network.
- One outbound address. A seed without public addresses reaches the internet through a NAT
server of your own (Servers without public addresses). Its
copies do the same, so all their outbound traffic leaves from the NAT server’s own public IPv4,
and that is the one address to allowlist. The NAT server is yours: Hetscale never creates,
changes or watches it, and while it is down none of the copies reaches the internet. New servers
take the public addresses the seed had when the group’s first release was taken, so set the seed
up this way before Go Live. A node group that is already Live
with a public seed keeps giving its new servers public addresses. In the route file, match the
interface by a pattern rather than by one name, for example
Name=e*in its[Match]section. Hetzner names a server’s private interface by its server type and by the order in which its networks were attached, such asens10orenp7s0for the first network andens11orenp8s0for the second (Hetzner’s server configuration page, read October 2026). The rule above still holds: the file’s name must sort before the one cloud-init writes on a copy’s first boot. - Keep the allowlist in sync.
GET /v1/groups/<group-id>/nodes, with a token that has thenodes:readability, lists the group’s current servers with theiripv4,ipv6andprivate_ip, including those still starting. A job that reads this list every minute can add a new address while the server starts, inside the readiness deadline, and remove an address once its server is no longer listed. With the webhook adapter,node.creatednames the new server’s addresses before your pipeline deploys, so the pipeline can add the address before it reports the server ready. On Pro and Team, an integration webhook set to Every event also tells you when servers come and go, for example withnode.bootingandnode.terminated(Integrations). Those deliveries name the server, and the inventory gives its addresses.
Adapter
The adapter is the way a node gets your application: snapshot (clone of the seed) or
webhook (a signed node.created event your pipeline acts on). A seed Coolify deploys to is
cloned by the snapshot adapter: Coolify keeps deploying to the seed and never sees the new nodes
(Coolify); a mode in which Coolify deploys each new node is planned. Both adapters
boot a copy of the seed’s disk, and a new node’s first boot runs one fixed cloud-init step that
removes Coolify’s monitoring container, Sentinel, if the copy holds it. The step carries no secret,
and it is the only thing of Hetscale’s that runs inside a node. Adapters are distribution
channels; the scaling engine does not depend on them.
Hetscale sends no user data of yours with a new server; the Sentinel step above is the only cloud-init it sends. Work a server must do at boot belongs on the seed, for example as a systemd unit, followed by a new release so that new servers start with it (How do I keep new servers patched?). Webhook servers boot the first release’s snapshot, so a change to the seed does not reach them; your pipeline does that work before it reports the server ready.
Traffic provider and readiness
The traffic provider is the load balancer in front of the group: a Hetzner Cloud Load Balancer
today; Cloudflare Load Balancing is planned behind the same seam. Its per-target health status is
the readiness authority. A node becomes ACTIVE, counts toward capacity and receives traffic
only when the provider reports it healthy. Adapter completion and ready callbacks never open
traffic on their own; with the webhook adapter a node joins the load balancer only after your
pipeline’s ready report. A node that never turns healthy before the deadline (default 15
minutes) is destroyed and reported with the load balancer’s reason.
Drain
Removing a node means taking it out of the load balancer, giving open requests the group’s drain window to finish, then shutting the server down and deleting it; a node is never deleted while it is still a target. The steps, the window and when in the billing hour a node leaves are on Removing a server without dropping requests.
Healing
A node that stays unhealthy at the load balancer while Hetzner does not report it as rebooting is failed: Hetscale starts a replacement beside it from the current release and moves traffic to it once it is healthy. What happens to the failed node, with a minimum of one, under overload and for the seed is on What happens when a server dies.
Placement groups
Every node group spreads the servers Hetscale creates across Hetzner “spread” placement groups: each such server runs on a different physical host, so one host failing takes down at most one of them. This applies to existing groups too; running servers are not moved until they are replaced, and the seed is never moved. A placement group holds up to ten servers, Hetzner’s limit, and Hetscale opens the next one as the group grows. A project has at most 50 placement groups, and a placement group can span locations.
Capacity comes first. When Hetzner cannot place a server apart from the others, the project cannot create another placement group, or the server’s placement group was deleted in the Hetzner Console, the server is created at once without one. The Servers page, whose “Placement” column shows each server’s placement group (pg-1, pg-2, …), marks it “Not spread”, and the timeline records it. Servers that were in a placement group deleted in the Console count as not spread too, because Hetzner takes them out of the group it deletes. Spread servers across hosts in the group’s Settings is on by default; turned off, new servers are created without a placement group and running servers stay where they are. Replace older servers moves a server that is not spread into a placement group: Releases, rollouts and rolling back.
Schedules
On Pro and Team, a group’s Schedules page raises its floor or lowers its ceiling on the weekdays and hours you set, in your organisation’s time zone, while autoscaling keeps working inside the window. How the rules combine and what the end of a window does are on Adding servers before a known peak.
Ownership labels
Every server Hetscale creates carries the labels hetscale.dev/managed=true,
hetscale.dev/group=<group-ulid> and hetscale.dev/node=<ulid> and a short name,
hetscale-<group prefix>-<number> such as hetscale-web-7: the prefix is chosen when observing
starts (by default from the group’s name) and the number counts up per group, never reused.
Before any destructive call, the labels, the name Hetscale recorded for the server and the ledger
must agree; anything else in the project is listed and never shut down or deleted. During creation
a fourth label, hetscale.dev/op, ties the server to the intent that requested it, so a retry
adopts a half-created server instead of creating a second one. The placement groups Hetscale
creates are named hetscale-<group prefix>-pg-<n> and carry hetscale.dev/managed,
hetscale.dev/group and hetscale.dev/op; Hetscale deletes one only when it is empty and its ledger
record, labels and name match, and never touches a placement group it did not create.
The group label holds the node group’s id without the grp_ the API puts in front of it: a group
the API calls grp_01k… has servers labelled hetscale.dev/group=01k…, in lower case. You can take
the id from GET /v1/groups (with a token that has the groups:read ability), or read the label of
a server Hetscale created in the group, in the Hetzner Console. Your own monitoring or inventory can
find those servers with the label selector
hetscale.dev/managed=true,hetscale.dev/group=<group-ulid> and a Read token of your Hetzner
project. The node group’s document in the API names it ready to use as label_selector, and the
group’s Servers page shows it below the list, to copy. Prometheus’s Hetzner service discovery takes it as label_selector in a
hetzner_sd_configs entry with the role hcloud, from Prometheus 3.5.0 on; relabel
__meta_hetzner_public_ipv4, or __meta_hetzner_hcloud_private_ipv4_<network-name> for a server
without a public IPv4, to the port of an exporter that runs on the seed and so on every copy. On an
older version, keep the targets whose __meta_hetzner_hcloud_label_hetscale_dev_group is the
group’s id. Ansible’s hetzner.hcloud.hcloud inventory plugin takes the same label_selector, in
a file whose name ends in hcloud.yml, with the token in HCLOUD_TOKEN
(Prometheus configuration,
the plugin’s source,
read October 2026). Each refresh is a request to the Hetzner API with your token; Prometheus
refreshes every 60 seconds by default. Those requests count toward your project’s hourly budget,
which Hetscale shares.
The selector finds only servers that carry Hetscale’s labels, in every phase from creation to
deletion: starting, serving, kept after a failure and leaving. It also finds any quarantined server
(Outside Hetscale, quarantined, gone from Hetzner). It does not
find the seed unless you promoted it, nor any other server of yours, so add the seed to your tool
on its own. For the servers that serve, the list to use is the inventory API with ?phase=active.
Never use the selector as a load balancer target or as a firewall’s label selector. A target by label sends traffic to every server that carries the labels as soon as that server passes the health check. That includes a quarantined server, which Hetscale keeps out of your load balancer, and, with the webhook adapter, a server your pipeline has not yet reported ready. A leaving server would also stay a target until it is deleted, so the drain window would not protect its connections. Hetscale adds and removes each server at your load balancer itself. Firewalls follow the same rule: new servers take the firewalls applied to the seed, by id, and a firewall applied by label would reach them in ways Hetscale does not control.
Modes
Observe reads only and records what it would have done. Rehearsal is a supervised rehearsal: one short-lived server is created, checked and deleted while you watch; Hetzner bills at least one started hour. Propose waits for your approval before each scaling action; healing acts on its own. Live acts inside your range. The mode decides whether Hetscale may call the Hetzner API; it never changes how a decision is made or explained. See Modes.