Cloudflare's Containers documentation describes a container reaching a KV namespace at http://my.kv/some-key as if it were a networking breakthrough: no IP address, no client library, just a hostname. Read the mechanism behind it and the breakthrough shrinks to something more specific — a Worker developer wrote a rule mapping that exact hostname to that exact binding, by hand, and every other hostname a container might want to reach needs its own rule.
That's a real feature. It is not service discovery. Kubernetes' ClusterIP Service has given any pod a resolvable hostname for any other service, automatically, protocol-agnostic, since the object was created — no per-hostname rule, no Worker in the middle. The interesting question isn't whether Cloudflare's version is convenient (it is); it's what a self-hosted, Cluster-API-provisioned PaaS gets for free that Cloudflare had to build a proxy layer to approximate.
What Actually Shipped
Cloudflare Containers reached general availability on April 13, 2026, during the company's Agents Week. The headline networking change didn't touch how a Worker reaches a container — that's been the same session-scoped lookup since public beta:
const containerInstance = getContainer(env.MY_CONTAINER, sessionId);
return await containerInstance.fetch(request);getContainer() takes a Durable Object binding and an ID, and returns a stub you call .fetch() on. There's no IP address anywhere in that call, and there wasn't one in the beta either — it's a logical, session-scoped lookup baked into the Durable Object binding from day one. Any framing that says the beta "required tracking a container's IP" doesn't match what's in Cloudflare's own docs or the @cloudflare/containers source.
What actually shipped — first as a March 26, 2026 changelog entry, expanded at GA — is the other direction: a container reaching out to a Workers binding. That's the outbound and outboundByHost handlers:
MyContainer.outboundByHost = {
"my.kv": async (request, env, ctx) => {
const key = new URL(request.url).pathname.slice(1);
const value = await env.MY_KV.get(key);
return new Response(value);
},
"my.other-container": async (request, env, ctx) => {
const target = getContainer(env.OTHER_CONTAINER, ctx.containerId);
return target.fetch(request);
},
};Code inside the container makes a plain HTTP request to http://my.kv/some-key. The outbound handler — which runs in the Workers runtime, not inside the container's sandbox — intercepts it and translates the request into whatever the developer wrote: a KV read, an R2 call, or, as the second entry shows, a hop to a different container instance. No SDK inside the container, no client library, no credential baked into the image.
The constraints matter as much as the capability. outbound/outboundByHost only intercept HTTP and HTTPS traffic on ports 80 and 443 — a container making a raw TCP connection, or talking on any other port, bypasses the handler entirely and just goes out to the public internet (or nowhere, if enableInternet is off). DNS queries are hardcoded to resolve only against Cloudflare's own servers, closing off DNS-tunneling exfiltration but also meaning there's no real service-discovery layer underneath the hostname — the hostname is a key in a lookup table the developer maintains, not a name the platform resolves. Cloudflare even has to fake the mechanism locally: wrangler dev spawns a sidecar process inside the container's own network namespace just so the same interception behaves the same way on a laptop as it does at the edge — a workaround a real cluster DNS system, which behaves identically in every environment because it's the same CoreDNS pod everywhere, never needs.
The Routing Table You Have to Write
Here's what that second entry in the outboundByHost map actually costs a developer wiring up two containers that need to talk: a named route, a target lookup, and a .fetch() call, per pair of services, per direction. Ten services that need to reach three others each is thirty hand-written route entries, one Worker deploy per change, and an outage waiting for the entry someone forgot to add.
Compare that to what a Cluster-API-provisioned Kubernetes cluster gives a git-push PaaS out of the box:
Cloudflare outboundByHost | Kubernetes ClusterIP Service + CoreDNS | |
|---|---|---|
| Setup per new callee | Hand-write a hostname → handler entry, redeploy the Worker | Nothing — the Service object is created once, at deploy time |
| Protocol support | HTTP/HTTPS only, ports 80/443 | Any TCP or UDP protocol, any port |
| Who resolves the name | The Worker developer's own routing table | CoreDNS, automatically, for every Service in the cluster |
| Backend tracking on reschedule | Developer's handler must call getContainer() with the right ID again | EndpointSlice updates automatically; the DNS name never changes |
| New service N+1 | New route entry, new redeploy | curl http://service-name just works |
The right column isn't a hypothetical bex has to build — it's what kube-proxy, CoreDNS, and the EndpointSlice controller already do the moment a Service object exists. A pod on a bex-managed cluster reaches another service the same way it would reach postgres.default.svc.cluster.local on any Kubernetes cluster anywhere: resolve the name, get routed to a healthy backend, done. Nobody wrote a per-hostname rule for it, and nobody has to update one when a pod reschedules to a different node.
The Problem Cloudflare Is Actually Solving
None of this is a knock on the engineering. Cloudflare built outboundByHost because Workers containers don't sit inside a cluster with a control plane that already tracks service membership — a Worker's container is an ephemeral sandbox invoked at the edge, with no Endpoints controller anywhere nearby to ask "who's currently healthy for my-service?" Giving developers a hostname-shaped escape hatch into bindings, instead of forcing them to bundle a KV or R2 SDK into every container image, is a genuine ergonomics win over the alternative Cloudflare actually had to choose between.
But it's an ergonomics win within Cloudflare's own architecture, not a networking primitive on par with what a real cluster provides. The tell is in the constraints: HTTP-only, two ports, hand-written per hostname. A DNS-based system doesn't care what protocol rides on top of the connection, and it doesn't need a developer to enumerate every caller-callee pair up front — it answers "where is X right now" for anything that asks, which is the actual definition of service discovery. outboundByHost answers "if you ask for exactly this hostname, run exactly this code" — a routing table, useful, but a different thing.
The Same Question Gets Worse When an Agent Is the One Deploying
Cloudflare shipped this at Agents Week for a reason: containers on Workers are increasingly the sandbox an AI agent spins up to run untrusted code, not just a human's long-lived backend service. That changes who has to write the routing table. A human developer wires up outboundByHost once, reviews it, ships it. An agent provisioning a new container per task — one sandbox to run a test suite, another to render a preview, a third to call out to a model server — would need to also generate and redeploy the Worker-side routing code every time it wants two of those sandboxes to talk, because the hostname map lives in the Worker, not in whatever manifest described the container.
That's the opposite of what makes agent-driven infrastructure tractable. An agent operating a Cluster-API-provisioned PaaS doesn't touch a routing table at all: it applies a manifest with a Service object, and every other workload in the cluster can resolve that Service's DNS name the instant the object exists — no redeploy, no separate routing artifact to keep in sync with the fleet the agent is actually managing. The fewer distinct systems an agent has to update to make one action (deploy a new service) fully effective (make it reachable), the fewer places a partially-applied change can leave the fleet in a broken, half-wired state. A hostname that resolves itself is a smaller surface for an agent to get wrong than a hostname somebody — human or agent — has to remember to also route.
What This Means for a Cluster-API Roadmap
The honest version of this comparison has to include where Cloudflare wins outright, because pretending otherwise would be the same kind of framing this piece is pushing back on. A Workers container needs zero cluster to run — no node pool, no control plane to keep patched, no kubeadm anywhere in the picture, and Active-CPU pricing bills only the CPU-seconds actually consumed, not a provisioned instance sitting idle. That's a real advantage for globally-distributed, bursty, stateless work, and it's not one a Cluster-API-based PaaS running on owned Hetzner hardware can match — bex bills for the node, not the request.
What a Cluster-API PaaS doesn't have to build, though, is the routing table. Every app deployed through bex lands in a namespace with a Service object and a DNS name the moment it's created, and every other app in the same cluster can already resolve it — no outboundByHost entry, no redeploy to add a new callee, no protocol restriction to HTTP on two ports. The thing Cloudflare's GA announcement frames as a networking upgrade is, from inside a Kubernetes cluster, table stakes that's been true since kube-dns shipped a decade ago. The gap this framing implies runs the other way.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, with Kubernetes Service discovery underneath it from the first deploy. Star the repo on GitHub or deploy your first app today.



