Somewhere in your cluster, right now, there are two ingress stacks. The first one serves HTTP: an ingress controller or a Gateway API implementation routing hostnames to apps, with TLS certificates, rate limits, and a dashboard you trust. The second one is everything else — the tenant database on a raw TCP port, the game server's UDP traffic, the QUIC media endpoint — each exposed through its own Service of type LoadBalancer, its own NodePort range, or a vendor-specific CRD that only one controller understands. Two routing APIs, two RBAC stories, two places to look when traffic stops flowing.
On June 30, 2026, Gateway API v1.6.0 graduated TCPRoute and UDPRoute to the Standard channel as stable v1 resources. For the first time, the same Gateway object that routes your HTTP apps can route raw TCP and UDP too — one GatewayClass, one route-ownership model, one status surface for all three protocols. If you are migrating off Ingress NGINX (unpatched since March 2026), this release means the migration can absorb the L4 sprawl too — not just the HTTP half.
This post gives you the concrete design: one Gateway serving an HTTP app, a TCP service, and a QUIC/UDP service — the YAML, the TLS decision table, the port and tenancy rules L4 forces on you, and when a separate L4 path still wins.
What v1.6 actually shipped
The facts, briefly, because the design below depends on them. Gateway API v1.6.0 was released on June 30, 2026, and announced on the Kubernetes blog on August 3 by Beka Modebadze (Google) and Ricardo Katz (Red Hat). Its headline change is the graduation of TCPRoute (GEP-2644) and UDPRoute (GEP-2645) from the Experimental channel to Standard, in the stable v1 API version. The v1alpha2 versions are deprecated and will be removed in a future release.
The announcement states the gap being closed about as plainly as a SIG ever does:
Until now, Gateway API only offered a stable routing model for HTTP and TLS traffic. Workloads that speak a raw protocol over TCP or UDP — databases, DNS, VoIP, gaming, IoT telemetry — had no portable way to plug into a Gateway. Users either fell back to a plain Kubernetes Service, or to an implementation-specific CRD that doesn't travel between Gateway controllers.
The key word is portable: L4 routing was always possible, but never vendor-neutral — a route written for one implementation's TCP CRD says nothing about the next. TCPRoute and UDPRoute route on protocol and port alone, no L7 awareness required, and any conformant implementation must honor them. On announcement day, six implementations already carried v1.6 conformance reports: Agentgateway, Airlock Microgateway, GKE Gateway, kgateway, NGINX Gateway Fabric, and Traefik Proxy.
Scope note: this post covers the design the graduation unlocks. If your fleet still has v1alpha2 L4 manifests, run the companion CRD-migration runbook first — everything below assumes gateway.networking.k8s.io/v1.
The design: one Gateway, three listeners
Picture a small self-hosted PaaS on machines you own. Three tenants need edge access: A ships a web app, B needs a Postgres-wire database over TCP 5432, C runs a QUIC-native API on UDP 443. Before v1.6, A got a route and B and C got bespoke plumbing. After v1.6, all three attach to one Gateway:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: paas-edge
namespace: platform
spec:
gatewayClassName: paas-gateway-class
listeners:
- name: web-http
protocol: HTTP
port: 80
allowedRoutes:
kinds:
- kind: HTTPRoute
namespaces:
from: All
- name: tenant-tcp
protocol: TCP
port: 5432
allowedRoutes:
kinds:
- kind: TCPRoute
namespaces:
from: Selector
selector:
matchLabels:
paas.example.com/tcp-access: "true"
- name: tenant-quic
protocol: UDP
port: 443
allowedRoutes:
kinds:
- kind: UDPRoute
namespaces:
from: Selector
selector:
matchLabels:
paas.example.com/udp-access: "true"Each tenant then attaches a route to their listener. Tenant A's web app uses the HTTPRoute you already know:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: tenant-a-web
namespace: tenant-a
spec:
parentRefs:
- name: paas-edge
namespace: platform
sectionName: web-http
hostnames:
- app-a.apps.example.com
rules:
- backendRefs:
- name: web-svc
port: 8080Tenant B's database gets a TCPRoute — the same parentRefs shape, no hostname matching, the backend port doing the real work:
apiVersion: gateway.networking.k8s.io/v1
kind: TCPRoute
metadata:
name: tenant-b-db
namespace: tenant-b
spec:
parentRefs:
- name: paas-edge
namespace: platform
sectionName: tenant-tcp
rules:
- backendRefs:
- name: db-svc
port: 5432And tenant C's QUIC endpoint gets a UDPRoute:
apiVersion: gateway.networking.k8s.io/v1
kind: UDPRoute
metadata:
name: tenant-c-quic
namespace: platform
spec:
parentRefs:
- name: paas-edge
sectionName: tenant-quic
rules:
- backendRefs:
- name: quic-svc
port: 443Follow a packet: it arrives at the Gateway's address on a port, the matching listener claims it, and the attached route selects the backend Service. HTTP paths can further match hostnames and paths; TCP and UDP paths deliberately match nothing more — port and protocol pick the backend, and the bytes flow. (Omitting sectionName from a parentRefs entry attaches the route to every listener of that protocol on the Gateway — convenient for HTTP, almost never what you want for TCP or UDP, where each listener is a distinct tenant port.)
Two simplifications to name before you copy this. First, production HTTP wants an HTTPS listener with a certificate beside web-http — that decision lives in the TLS section. Second, the UDPRoute sits in platform while the other routes live in tenant namespaces; both shapes are legal, and the difference — who you trust to mint routes — is the tenancy section's subject.
TLS at layer 4: terminate vs passthrough
The first question about L4 routes is where TLS ends. TCPRoute and UDPRoute have no TLS fields at all — they move bytes, and the endpoints arrange encryption. That leaves four patterns, and picking the wrong one is the easiest way to misdeploy the new routes:
| Traffic | Mechanism | TLS ends at | When to use it |
|---|---|---|---|
| Browser HTTPS | HTTPS listener + HTTPRoute | Gateway (edge cert) | Unchanged from before v1.6; still the default for web tenants |
| TLS-over-TCP, many backends on one port | TLSRoute (Standard since v1.5) | Gateway (Terminate) or backend (Passthrough) | SNI-based routing when several TLS services share a port |
| Raw TCP (database wire, custom protocol) | TCPRoute | Backend app | No SNI multiplexing needed; run TLS in the backend or accept plaintext inside your trust boundary |
| QUIC or DTLS over UDP | UDPRoute | App, always | QUIC embeds its TLS 1.3 handshake inside UDP datagrams, so a port-and-protocol proxy cannot terminate it without becoming QUIC-aware — plan on passthrough |
TLSRoute is the right tool when one TCP port must fan out to several TLS backends by SNI hostname — it exists precisely because TCPRoute cannot see hostnames. For gateway-to-backend encryption behind an edge-terminating listener, BackendTLSPolicy configures how the Gateway originates TLS to backend pods; see the Gateway API TLS guide for the full matrix.
The QUIC row deserves emphasis because it surprises HTTP-trained intuition. A UDP proxy that forwards datagrams by destination port is transparent to QUIC: connection migration, congestion control, and the embedded TLS handshake all survive untouched, because the proxy never parses them. That transparency is a feature — it is also why "terminate QUIC at the edge" is not on the menu unless your implementation ships an explicit QUIC-aware mode. Terminate in the app, and let the UDPRoute be dumb pipes.
What HTTP gave you for free: ports, hostnames, and tenant isolation
Collapsing three stacks into one Gateway is straightforward YAML. Operating it multi-tenant is where HTTP's conveniences turn out to have been load-bearing. Three rules:
Rule 1: budget ports like IP addresses. An HTTPRoute multiplexes unlimited hostnames onto one listener port; a TCP or UDP listener routes by port alone, so each tenant L4 service consumes a listener port on the Gateway's address. Treat the port space as platform inventory: reserve documented ranges for tenant TCP and UDP services, create one listener per (port, protocol) pair in the platform-owned Gateway manifest, and never let tenants mint listeners. The SNI exception (TLSRoute) is the only L4 multiplexing you get — everything else is one port, one tenant.
Rule 2: listeners are yours, routes are theirs. The allowedRoutes stanza is the ownership boundary. The platform team owns the Gateway and decides, per listener, which route kinds and which namespaces may attach — from: All for the shared HTTP listener, a label selector for the scarcer L4 ports, as in the example above. Tenants create Route objects in their own namespaces pointing at your listeners, and cross-namespace backends additionally require a ReferenceGrant from the backend's namespace. Corollary: two TCPRoutes on one listener have no hostname to split traffic by, so treat each L4 listener as single-route and enforce it with admission policy — first writer wins is not a tenancy model.
Rule 3: quotas live outside the routes. Gateway API defines no per-tenant route quota, but Kubernetes ResourceQuota counts custom resources just fine: count/tcproutes.gateway.networking.k8s.io, count/udproutes.gateway.networking.k8s.io, and their HTTP counterparts cap how many routes a namespace may hold. Pair that with a policy constraining which sectionName values each namespace may reference, and onboarding is self-serve: label the namespace, apply the route, watch Accepted and Programmed go true.
Reality check on owned hardware
Three things to verify before you collapse your split stack, in order of how often they bite.
First, the controller is the long pole, not the CRDs. v1.6 conformance on announcement day covered six implementations — Agentgateway, Airlock Microgateway, GKE Gateway, kgateway, NGINX Gateway Fabric, and Traefik — with the ecosystem mid-migration behind it: the AWS Load Balancer Controller's NLB Gateway support moved to the v1 L4 APIs, and the APISIX ingress controller now requires Gateway API 1.6+ for L4 routing. Read your GatewayClass implementation's conformance report and confirm it watches the v1 L4 types before you convert a single manifest; a cluster serving routes no controller reconciles is an outage wearing a green CI badge.
Second, the Gateway still needs an address from something below it. Gateway API provisions routes, not IPs. Beneath your Gateway there is still a mechanism handing it a public address — a cloud load balancer, MetalLB speaking BGP or ARP on bare metal, NodePort or hostNetwork at the scrappy end. "No second load balancer" means no second routing API: the packet arrives over whatever L2/L3 path you already operate, and one Gateway object decides where it goes. On a rack of owned machines that usually means MetalLB (or your provider's LB) in front of exactly one Gateway deployment, not one LB per protocol.
Third, confirm the statuses, not just the applies. Every route type reports Accepted and Programmed; a route that applies cleanly but never programs is a DNS record pointing at a dead IP. Check conditions on all three route types — and controller logs for version-skew errors — before declaring victory.
From split stack to one Gateway
Ordered, boring, reversible at every step:
- Inventory the sprawl. List every non-HTTP exposure:
LoadBalancerServices,NodePortallocations, vendor L4 CRDs. Each one becomes a TCP or UDP listener candidate with a known port and tenant. - Stand up the Gateway HTTP-first. Deploy the Gateway with your existing HTTP listeners and move web traffic first — behavior-preserving, and it proves the
GatewayClassbefore L4 rides on it. - Add one L4 listener per protocol, non-prod first. Carve the TCP and UDP listeners for a staging tenant, attach the routes, and watch
Accepted/Programmedplus real traffic before touching production ports. - Encode Rules 1–3 as policy. Port ranges,
allowedRoutesselectors,ResourceQuotacounts, admission checks onsectionName. The next tenant should onboard with a labeled namespace and one route manifest. - Delete the retired Services last. The old Services cost IPs and pennies while they exist, but they are also your instant rollback — remove them only after the L4 listeners have carried production for a full deploy cycle.
If any manifest in the inventory still says v1alpha2, do the CRD migration before step 2 — the companion runbook has the skew matrix and the six-step upgrade.
One stack by default, two when earned
The case for collapsing is operational surface: one routing API to learn, one RBAC model to audit, one status surface to alert on, and routes that survive a change of Gateway implementation because they are standard resources instead of vendor CRDs. For the modal self-hosted PaaS — HTTP apps plus a database wire here and a QUIC endpoint there — that is the right default now that the API is stable.
Keep a separate L4 path when you can name the reason. Sub-millisecond UDP workloads such as game servers often bypass proxying entirely in favor of direct-to-node routing, where any extra hop is measurable latency. Cloud-NLB-specific features — AWS's QUIC Connection-ID-aware passthrough mode, added in November 2025, is the canonical example — may beat a portable route on behavior you actually need.
Regulated database exposure sometimes wants network-level isolation no route object expresses. One stack by default; two when earned, with the reason written down.
Looking forward, the project's energy moves to what v1.6 left alone: experimental XBackend takes a first step toward Gateway-managed egress for agentic workloads, and the x-k8s.io split means the next graduation arrives as a clean rename. The L4 story itself is done being experimental — if your TCP and UDP traffic still lives in a parallel universe of Services and annotations, v1.6 is the release that invites it home.
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.
Sources
- Kubernetes Blog, "Gateway API v1.6: TCPRoute and UDPRoute Graduate to Standard" (Aug 3, 2026, Modebadze/Katz): v1.6.0 released June 30 2026,
v1graduation andv1alpha2deprecation, the "no portable way" gap statement, listener/attachment semantics, day-one conformance list, XBackend preview. - Gateway API GEP-2644 (TCPRoute) and GEP-2645 (UDPRoute): route semantics and graduation criteria.
- Gateway API user guides for TCP and UDP routing, and the TLS guide.
- kubernetes-sigs/gateway-api v1.6.0 release notes.
- Kubernetes Blog, "Announcing Ingress2Gateway 1.0" (Mar 20, 2026): Ingress NGINX retirement schedule and migration tooling.
- kubernetes-sigs/aws-load-balancer-controller PR #4829: NLB Gateway migration to
v1TCPRoute/UDPRoute. - AWS What's New (Nov 13, 2025): NLB QUIC protocol support in passthrough mode.



