Skip to main content

TCPRoute Went Stable, but Somebody Still Has to Own the IP: kube-vip vs Talos BGP vs Hetzner LBaaS

10 min readDora NodaDora Noda
Share
On this page

Gateway API v1.6 graduated TCPRoute and UDPRoute to the Standard channel in June 2026, and for the first time a git-push PaaS can route raw TCP — game servers, tenant databases, custom protocols — through portable, vendor-neutral route objects instead of a per-controller CRD. There is exactly one catch, and it lives one layer below the route: a TCPRoute decides where packets go, but something still has to own the destination IP and announce it to the network. On owned hardware there are three honest answers, and they differ by an order of magnitude in cost the moment you have more than one TCP service.

Here is the verdict up front, for the concrete case of a Cluster API fleet on Hetzner exposing N tenant TCP services:

OptionCost per TCP service1 / 5 / 20 servicesFailoverSubnet scope
kube-vip ARP/VRRP virtual IP€0 (DaemonSet, no extra infra)€0 / €0 / €0Seconds, via gratuitous ARP; single ingress nodeOne L2 broadcast domain only
Talos v1.14 embedded GoBGP announcement€0 (in-OS speaker)€0 / €0 / €0BGP withdrawal, sub-second to seconds; ECMP possibleCross-subnet, wherever your router peers
Hetzner Cloud LB per service~€7.49/mo per LB (LB11 list, post-April 2026)~€7.49 / ~€37.45 / ~€149.80Managed failover, health-checkedAnywhere in the project/region

The free options stay free at any service count; the managed option scales linearly with every TCP port you expose. That single row is the whole budgeting conversation — and it is also why the deprecation of Service externalIPs in Kubernetes 1.36, with removal planned around v1.43, turns this from a nice-to-know into a migration you have to schedule: the cheap hack a lot of bare-metal fleets used to pin an IP to a Service is on a clock.

TCPRoute going GA solved routing, not addressing

Until v1.6, TCP and UDP routing in Gateway API lived on the experimental channel as v1alpha2 objects. Operators who wanted portable L4 routing either pinned to an alpha API or fell back to implementation-specific CRDs that do not travel between gateway controllers. The v1.6 release — shipped June 30, 2026, with the SIG Network write-up landing August 3 — promotes both route types to gateway.networking.k8s.io/v1 on the Standard channel and starts the deprecation clock on the v1alpha2 shapes. Manifests referencing the old version keep working for now, but the countdown to removal has started, so anything new should be written against v1.

What that promotion does not give you is an IP. A Gateway, a listener, and a TCPRoute together describe intent: "traffic arriving at this address and port goes to these backends." None of those objects puts an address on the wire. In a hyperscaler that gap is invisible because a LoadBalancer Service conjures a cloud IP in seconds. On owned hardware — Hetzner dedicated boxes, a colo rack, a homelab that grew teeth — there is no cloud to conjure from. Either a node claims the IP itself and tells its L2 neighbors about it, a host speaks BGP and tells the router, or you rent a managed load balancer per service and let it hold the address. Those are the three rows in the table, and the rest of this post prices each one.

Option 1: kube-vip and the ARP announcement (€0, one LAN)

kube-vip is the smallest possible answer. It runs as a static pod or DaemonSet on control-plane (and optionally worker) nodes, elects a leader for each virtual IP, and has the winner claim the address with gratuitous ARP (or NDP on IPv6). From the switch's perspective, the VIP just lives on whichever node is currently leader. No extra daemon, no cloud API, no per-service fee — which is why it is the default HA-API-endpoint recipe in most kubeadm and bare-metal guides, and why kubeadm's own HA considerations list it alongside keepalived/haproxy as the no-external-LB option.

For a single-subnet fleet it works fine, including for tenant TCP Services, not just the API server. But the limits are structural, not tuning:

  • One LAN only. ARP announcements do not cross a router. If your nodes span subnets — separate racks, separate Hetzner private networks, anything routed — the standby node in the other subnet cannot credibly claim the same L2 address. This is the exact seam where VRRP setups stumble and BGP takes over.
  • One ingress node per VIP. All traffic for a given VIP flows through the current leader, which then forwards to backends. That is fine for an API endpoint; it becomes a bandwidth and blast-radius bottleneck when it carries every tenant's TCP traffic, and a slow or failed failover means the whole VIP goes quiet until gratuitous ARP re-converges.
  • A hard ceiling of 255 VIPs per LAN. VRRP router IDs are 8 bits, so a single broadcast domain tops out at 255 VRRP instances unless you split VLANs. Twenty tenant TCP services fit; two hundred do not, at least not on one flat network.
  • Privileged networking. kube-vip and keepalived-class agents need NET_ADMIN and NET_RAW to manipulate interfaces and emit ARP frames — expected on a host-networked static pod, but worth naming in a multi-tenant threat model: the component that owns your VIPs owns raw L2.

Use kube-vip when the fleet lives on one L2 network, the TCP service count is modest, and €0 with seconds-long failover beats paying per service. Stop using it when tenants span subnets or the single-ingress-node bottleneck shows up in your p99s.

Option 2: Talos v1.14 speaks BGP from the OS (€0, routed networks)

Talos Linux v1.14 (beta landed July 31, 2026) embeds a GoBGP speaker directly in the OS, configured through BGPInstanceConfig documents. Instead of shouting on L2, each host routes: it announces the Service IPs it owns to a configured BGP peer — typically a top-of-rack switch or an upstream router — which then forwards toward the announcing node. Withdraw the announcement and traffic reconverges through normal BGP path selection, including ECMP across multiple announcers when the topology allows it.

That changes the two things kube-vip cannot do. Announcements are L3, so they cross subnets freely: a tenant TCP service on rack A and another on rack B each announce their own prefixes to the same router fabric, and no broadcast domain has to stretch to cover both. And there is no single ingress leader per VIP by construction — every node holding a replica can announce, so the router, not an election, spreads the load.

The price is a peer. BGP is a conversation, and the host needs something to converse with: a ToR that speaks BGP, a virtual router you operate, or an upstream that accepts your announcements. On Hetzner dedicated servers with your own switch or a vSwitch topology you control, that is a solvable engineering task. On Hetzner Cloud — the default private-network + Cloud LB world most Cluster API Hetzner fleets actually run on — there is no BGP peer to dial: the platform gives you L2 private networks and managed load balancers, not a route server for customer announcements. So the honest form of this option is disjunctive: if your network team (or your rack) can peer, Talos BGP is the cheapest correct answer for routed fleets; if you live inside Hetzner Cloud's abstractions, it is architecturally unavailable and the choice collapses to kube-vip versus paying for LBaaS.

Option 3: a Hetzner Cloud load balancer per service (~€7.49/mo each)

The managed answer needs no L2 tricks and no BGP peer: create one Hetzner Cloud Load Balancer per exposed TCP service, point its target pool at the worker nodes, and let the platform hold a stable public IP with health-checked failover. It works from day one, integrates with the Hetzner Cloud Controller Manager so a LoadBalancer Service provisions itself, and moves failover responsibility off your team entirely.

It also bills per balancer. Hetzner's list price for the entry LB type moved to roughly €7.49 per month after the April 2026 adjustment (older guides still quote ~€5.39–5.50 from before the change; pin your estimate to the current price list for your location). The math is linear, which is the point of the verdict table:

TCP services exposedMonthly LB cost at ~€7.49 each
1 (one tenant database)~€7.49
5 (a few game servers plus a database)~€37.45
20 (a real multi-tenant TCP surface)~€149.80

Against a €4.49 Hetzner CX22-class box, a single LB costs more than a whole spare node, and twenty of them cost more than a respectable worker pool. None of that makes LBaaS wrong — for one or two revenue-critical TCP endpoints, managed health checks and zero L2/BGP operations are easily worth €7.49. It makes it a per-service tax that compounds exactly as your TCP surface grows, which is the sensitivity analysis the free options never need: kube-vip and Talos BGP cost the same €0 whether you expose one TCPRoute or fifty.

Why this stopped being optional: externalIPs is on a removal clock

A lot of bare-metal fleets never picked one of the three because a fourth hack worked: stuff an address into Service.spec.externalIPs and let kube-proxy program it. That field is now formally deprecated as of Kubernetes 1.36, with full removal expected around v1.43, for a reason that predates the deprecation by years — CVE-2020-8554 showed that unvalidated external IPs on Services enable man-in-the-middle interception of cluster traffic. Upstream's direction is explicit: conforming implementations must not rely on it, AKS Automatic already blocks it with admission policy, and every 1.36+ apiserver emits deprecation warnings when you use it.

TCPRoute's graduation and externalIPs' deprecation arrive together, and together they redraw the map: the portable way to route TCP is now stable, and the unportable way to address it is dying. Any fleet still pinning tenant TCP exposure on externalIPs needs one of the three announcements above before the removal release — there is no fourth option coming.

Which one for your fleet

  • Single-subnet fleet, handful of TCP services, cost-sensitive: kube-vip. €0, one manifest, failover in seconds. Watch the 255-VRID ceiling and the single-ingress bottleneck; both arrive gradually and then suddenly.
  • Routed bare metal with a BGP-capable ToR or virtual router: Talos v1.14 BGP. Also €0, but cross-subnet by design with router-grade reconvergence and ECMP. Budget the peering work, not the hardware.
  • Hetzner Cloud, one or two critical TCP endpoints, no network team: LBaaS. Pay the ~€7.49 per service gladly — it is cheaper than the outage you would debug at 3 a.m. Revisit the moment the LB line item exceeds a worker node.
  • Hetzner Cloud, many tenant TCP services: this is the uncomfortable cell, because Talos BGP cannot peer and twenty LBs cost €150/mo. The practical answers are kube-vip inside one private network (accepting L2 scope), consolidating TCP services behind fewer frontends (one Gateway IP, many TCPRoutes by port/SNI), or moving the TCP-heavy tenants to routed dedicated hardware where BGP is available. Port-based multiplexing deserves emphasis: Gateway API lets many TCPRoutes share one listener address, so N services need not mean N balancers — share the LB and split by port.

Whichever cell you are in, write new TCP exposure as gateway.networking.k8s.io/v1 TCPRoutes today. The v1alpha2 shapes are deprecated in 1.6 and will be removed; the announcement layer underneath them is the part you get to choose deliberately instead of inheriting.

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.

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