On September 8, Hetzner put a seven-week fuse on one of the most-read fields in its Cloud API: the deprecated response field on Images, Server Types, and Load Balancer Types disappears on November 2, 2026. The replacement is the structured deprecation object — and for server types, the per-location deprecation object that has been the blessed path since September 2025.
Here is the part that should make every fleet operator sit up: nothing will error. When the field vanishes from API responses, every client library will keep decoding successfully and hand you the zero value — which reads as "not deprecated." Your capacity checks, image guards, and Terraform check blocks won't fail loudly on November 2. They will pass quietly, forever, for everything. That is the audit this post walks through: what exactly changes per resource, where a Cluster API fleet actually branches on the old field (CAPH, Terraform, homegrown tooling), and the exact replacement for each.
Three resources, two legacy shapes, one deadline
The first thing to get straight is that the dying field isn't one shape. Hetzner's own OpenAPI spec shows two different legacy types under the same name, and the migration target differs per resource:
| Resource | Legacy deprecated (dies Nov 2) | Migration target |
|---|---|---|
| Images | Nullable RFC 3339 date string — the point in time the image counts as deprecated | Top-level deprecation object (announced, unavailable_after); null means not deprecated |
| Server Types | Boolean | deprecation object inside the matching locations[] entry — the top-level deprecation object is itself deprecated since the September 2025 per-location change |
| Load Balancer Types | Nullable RFC 3339 date string | Top-level deprecation object, added June 5, 2026 |
The server-types row is the trap. There are two deprecated things stacked there: the old boolean, and the top-level deprecation object that replaced it everywhere else. If your migration stops at "read deprecation instead of deprecated" for server types, you have migrated onto a field that is itself already sunset — the per-location entries are the only current source of truth. A server type can now be deprecated in fsn1 while fully available in ash, so any global boolean was always going to be a lie; November 2 just makes it an absent one.
The failure mode is silence, not errors
Most API removals announce themselves. Hetzner's datacenter endpoint shutdown returns HTTP 410 Gone after October 1. Missing JSON fields, by contrast, decode cleanly in every mainstream client: Go's encoding/json leaves the struct field at its zero value, Python's .get("deprecated") returns None, and jq yields null.
Look at what those zero values mean in hcloud-go's schema package:
Image.Deprecatedis a*time.Time: after removal it decodes tonil, which every "is this image deprecated?" check reads as no.ServerType.Deprecatedis a plainbool: after removal it decodes tofalse— not deprecated, unconditionally.
So the concrete failure is a deprecation guard that can never fire. Concretely, that matters because unavailable_after has teeth: once that date passes, the resource is no longer returned by list endpoints and cannot be used to create new servers. There is a live example in the fleet right now. Hetzner deprecated the Debian 11 image on August 31 (IDs 45557056 on x86, 103907373 on arm), with unavailable_after set to November 30, 2026 — four weeks after the field removal. Any image-pinning logic still consulting the legacy field will sail past both dates insisting Debian 11 is fine, right up until server creates start failing. The openSUSE 15 image already crossed its unavailable_after (July 30); it is gone from listings whether your tooling noticed or not.
November 2 is therefore a double deadline in disguise: migrate the readers, and make sure the migrated readers are actually consulted before the Debian 11 cutoff lands at the end of the month.
CAPH audit: the controller is clean, your pins are not
If you run a Cluster API fleet on Hetzner with cluster-api-provider-hetzner (CAPH), start with the good news, verified against CAPH's current source: the controller itself does not branch on the legacy field in either selection path.
Image path — no branch, verdict: check your pins, not the controller. CAPH's getServerImage (in pkg/services/hcloud/server/server.go) resolves HCloudMachine.Spec.ImageName by listing images filtered by label selector, then by name plus the server type's architecture, and errors only on ambiguous or missing results. There is no deprecation read anywhere in that flow. That means November 2 changes nothing about how CAPH picks an image — but it also means CAPH will happily keep referencing a deprecated snapshot or system image until the API refuses the create. Your audit items are the inputs, not the code:
- Every
imageNamepinned inHCloudMachineTemplatespecs: is it a system image with anunavailable_afteron the horizon (Debian 11, most urgently), or a snapshot your own pipeline rebuilds? - The snapshot pipeline itself: if you bake node images from a base that goes deprecated, the pipeline needs the
deprecationobject check, because the controller will never do it for you. - CAPH's vendored
hcloud-go(v2.32.0 at the time of writing): fine as a transport — the new fields decode today — but any fork or wrapper code touching.Deprecatedneeds the §6 treatment.
Server-type path — no branch, verdict: check the type string and its location. The controller passes HCloudMachine.Spec.Type straight through to the server-create call without reading availability or deprecation state. So again, no controller change is needed — but a type: cx22-style pin combined with a location where that type gets deprecated will fail at create time with an API error, not a friendly condition. Audit the pinned type against the per-location matrix (locations[].deprecation, locations[].available) for the location your HetznerCluster actually deploys into, especially if you recently moved regions: per-location deprecation means a type that was safe in Nuremberg may not be safe in Ashburn.
Terraform audit: the provider did the migration, you do the call sites
The Terraform provider story is the most mature of the three, because provider v1.53.0 shipped the per-location server-type migration a year ago, complete with a Before/After upgrade guide. The audit here is about your call sites and your version pin.
Server types — move the check block per location. If any module still asserts on the global attribute, the provider's own guide shows the shape of the fix:
# Before: global boolean (tracks the dying field)
data "hcloud_server_type" "main" {
name = "cx22"
}
check "server_type" {
assert {
condition = !data.hcloud_server_type.main.is_deprecated
error_message = "Server Type ${data.hcloud_server_type.main.name} is deprecated"
}
}# After: per-location deprecation for the location you deploy into
data "hcloud_location" "main" {
name = "fsn1"
}
data "hcloud_server_type" "main" {
name = "cx22"
}
locals {
server_type_location = one([
for o in data.hcloud_server_type.main.locations : o
if o.name == data.hcloud_location.main.name
])
}
check "server_type_location" {
assert {
condition = local.server_type_location != null
error_message = "Server Type ${data.hcloud_server_type.main.name} does not exist in Location ${data.hcloud_location.main.name}"
}
assert {
condition = !local.server_type_location.is_deprecated
error_message = "Server Type ${data.hcloud_server_type.main.name} is deprecated in Location ${data.hcloud_location.main.name}"
}
}Note the pair does double duty the old global boolean never could: the first assert catches "type doesn't exist here" (a region move, not a deprecation), the second catches "type deprecated here." Pin the provider to at least v1.53.0 wherever these data sources appear — older pins predate the locations attribute entirely, and those modules are exposed to both the November 2 removal and the per-location semantics.
Images and load balancer types — switch to the object-backed attributes. The hcloud_image / hcloud_images data sources expose is_deprecated and unavailable_after backed by the new object; prefer asserting on unavailable_after where you need lead time rather than a binary flag:
data "hcloud_image" "node" {
name = "debian-12"
}
check "node_image" {
assert {
condition = !data.hcloud_image.node.is_deprecated
error_message = "Node image ${data.hcloud_image.node.name} is deprecated"
}
}Grep every module for is_deprecated on a server type data source without a .locations qualifier — that is the exact pattern that goes stale — and confirm no module pins a provider older than v1.53.0.
Homegrown tooling audit: grep, then rewrite the reader
Everything above was maintained by someone else. This section is about the capacity scripts, image-guard cron jobs, and cost dashboards your team wrote against the raw API or a low-level client — the code with no upstream maintainer to migrate it for you.
Step 1: find the readers. These three greps cover the overwhelmingly common cases:
# Go (hcloud-go v1 or v2): legacy struct fields
grep -rn "\.Deprecated\b" --include='*.go' . | grep -v _test.go
# Python (hcloud-python / raw requests): legacy response key
grep -rn "deprecated" --include='*.py' . | grep -vi "deprecation\b" | grep -vi "^.*#"
# Shell / jq against the raw API: legacy JSON key
grep -rn "\.deprecated\b" --include='*.sh' --include='*.jq' .Treat every hit as guilty until proven innocent: after November 2 each of these reads a key that no longer exists and gets the zero value documented in the previous sections.
Step 2: rewrite the server-type reader per location. The canonical before/after in Go, using the same hcloud-go v2 types CAPH vendors:
// Before: global boolean — silently false for everything after Nov 2
serverType, _, err := client.ServerType.GetByName(ctx, "cx22")
if err != nil {
return err
}
if serverType.Deprecated {
return fmt.Errorf("server type %s is deprecated", serverType.Name)
}// After: per-location deprecation for the location you deploy into
serverType, _, err := client.ServerType.GetByName(ctx, "cx22")
if err != nil {
return err
}
for _, loc := range serverType.Locations {
if loc.Location.Name == locationName && loc.IsDeprecated() {
return fmt.Errorf("server type %s is deprecated in %s (unavailable after %s)",
serverType.Name, loc.Location.Name, loc.UnavailableAfter().Format(time.RFC3339))
}
}The same shape applies to images and load balancer types, minus the location loop: image.IsDeprecated() (hcloud-go), image["deprecation"] is not None (Python), .deprecation != null (jq).
Step 3: fix the watch, not just the code. The deeper lesson of a seven-week fuse is process. Hetzner publishes a changelog RSS feed; the September 8 entry sat there for anyone subscribed. If your fleet learned about November 2 from this post instead of from your own dependency watch, the durable fix is a recurring job — a weekly digest of the feed, a Dependabot-style alert on hcloud-go releases, or at minimum a calendar reminder to re-run the greps above quarterly. Strict decoders deserve one explicit warning: if any tooling uses a JSON schema or typed client that requires the deprecated key, it flips from "silently wrong" to "loudly broken" on removal day — better, but only if someone is watching the alerts.
The week after the fuse
November 2 removes the field; November 30 removes Debian 11. Between those two dates, every unmigrated reader is a guard that cannot fire, guarding an image that is actually expiring. The audit order that falls out of this post: Terraform call sites and provider pins first (they are the fastest to fix and the most likely to encode fleet policy), homegrown readers second (the only code nobody else will fix for you), CAPH pins third (the controller is clean; the imageName and type strings are the surface). None of these takes more than an afternoon, and all of them are dramatically cheaper than debugging failed server creates in December while wondering why every dashboard still says "not deprecated."
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.



