Gateway API v1.6 TCPRoute and UDPRoute vs LoadBalancer Services and Vendor CRDs

Gateway API v1.6 TCPRoute and UDPRoute vs LoadBalancer Services and Vendor CRDs

Gateway API v1.6 TCPRoute and UDPRoute vs LoadBalancer Services and Vendor CRDs

For eleven years, Kubernetes had a first-class, portable answer for HTTP and no answer at all for everything else. If you needed to expose an MQTT broker, a Postgres primary, a DNS resolver or a game server, you either burned a cloud load balancer and a public IP per service, or you adopted a vendor-specific custom resource and accepted that your manifests would not survive a controller migration. Gateway API v1.6 finally closes that gap: TCPRoute and UDPRoute graduated to the Standard channel and to v1 in the release shipped on 30 June 2026, announced on the Kubernetes blog on 3 August 2026. That means conformance tests, a stable API contract, and manifests that move between controllers. It also means a set of new constraints that are easy to discover the hard way.

What this covers: what the L4 route types actually do at the API level, how they compare with Service type=LoadBalancer and with vendor CRDs on portability, TLS handling, policy, RBAC and cost, two worked examples drawn from industrial IoT — multi-tenant MQTT over TLS and a Postgres connection pooler — and an honest read of which controllers ship conformant support today.

Context and Background

The Ingress API only ever modelled HTTP. That was a deliberate scoping decision in 2015 and a reasonable one, but it left a large, permanent hole. Anything speaking a non-HTTP protocol had three bad options.

The first was Service type=LoadBalancer. It works everywhere, and that is the whole of its virtue. Each Service provisions its own cloud load balancer and its own external IP. There is no hostname matching, no Server Name Indication (SNI) routing, no shared listener. Cross-cutting configuration lives in cloud-specific annotations that differ between AWS, GCP, Azure and on-premises implementations, so the manifest is portable in name only.

The second was a vendor custom resource. Traefik has IngressRouteTCP with its HostSNI matcher. Istio expressed the same intent through Gateway plus VirtualService with a tcp or tls block. ingress-nginx bolted L4 on through a --tcp-services-configmap flag that mapped an external port to a namespace/service:port string — a ConfigMap, not an API object, with no status, no validation and no namespace-scoped ownership. Each of these worked well. None of them transferred.

The third was to give up and run a separate L4 proxy outside the cluster.

Two things made this hole urgent in 2026. The first is that kubernetes/ingress-nginx reached end of life on 24 March 2026 and its repository is now read-only, with no further CVE patches; its intended successor, InGate, was retired before reaching maturity. A very large installed base has to migrate somewhere, and a meaningful slice of that base uses the TCP services ConfigMap. The second is that IoT and data platforms have been pushing brokers and databases into Kubernetes for years, and those are exactly the workloads that speak TLS-wrapped binary protocols rather than HTTP. If you are already weighing the broader migration, the Gateway API versus Ingress comparison covers the L7 half of this story; this post is about the half that had no story at all.

Gateway API reached GA for its HTTP types in v1.0 in October 2023. TLSRoute moved to the Standard channel in v1.5.0. TCPRoute and UDPRoute were the last core route types sitting in the Experimental channel, and v1.6 moved them across. As the upstream release notes put it, both “graduated to GA”, the v1 API version is now recommended, and the v1alpha2 version of each is deprecated and slated for removal.

How Gateway API Models Layer 4

Gateway API handles raw TCP and UDP by pushing all the matching logic into the Gateway’s listener and leaving the route object almost empty. A listener declares a protocol and a port; a TCPRoute or UDPRoute attaches to that listener and names one to sixteen backends. There are no hostnames, no matches and no filters on an L4 route, because TCP and UDP carry no application metadata to match on.

Gateway API v1.6 reference architecture routing MQTT, Postgres and DNS through TLS, TCP and UDP listeners

Figure 1: One Gateway, three listener protocols, three route kinds. The listener does the classification; the route only picks backends.

The diagram shows the shape that makes this worth adopting: a single Gateway resource, fronted by one cloud load balancer and one external IP, carrying a TLS listener on 8883 in Passthrough mode for MQTT, a TCP listener on 5432 for Postgres, and a UDP listener on 53 for DNS. Each listener admits a different route kind. The TLSRoute fans out to per-tenant broker Services by SNI hostname; the TCPRoute and UDPRoute each point at a single backend Service. What used to be three load balancers and three IPs is now one of each, and every routing decision is expressed in a namespaced API object with a status subresource.

The listener is the matcher, and that changes how you design ports

A TCPRoute attaches to a Gateway through parentRefs. If it names neither sectionName nor port, it attaches to every TCP listener on that Gateway — listeners using other protocols are unaffected. To bind to one listener you set sectionName to the listener’s name, or you set port to the listener’s port number.

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: platform-l4
spec:
  gatewayClassName: example-gateway-class
  listeners:
    - name: postgres
      protocol: TCP
      port: 5432
      allowedRoutes:
        kinds:
          - kind: TCPRoute
    - name: kafka
      protocol: TCP
      port: 9092
      allowedRoutes:
        kinds:
          - kind: TCPRoute
apiVersion: gateway.networking.k8s.io/v1
kind: TCPRoute
metadata:
  name: postgres-primary
spec:
  parentRefs:
    - name: platform-l4
      sectionName: postgres
  rules:
    - backendRefs:
        - name: pgbouncer
          port: 6432

The upstream documentation is explicit that sectionName is preferred over port. Binding by port number couples the Gateway and its routes: the platform team can no longer move a listener to a different port without editing every route that references it. Binding by listener name keeps the port a Gateway-level implementation detail. In a multi-team cluster that is not a style preference — it decides whether a port change is a one-line infra PR or a coordinated change across every application namespace.

allowedRoutes is the other half of the contract. On each listener it constrains which route kinds may attach and, through allowedRoutes.namespaces, which namespaces they may come from. A TCPRoute cannot attach to an HTTP or HTTPS listener at all; the spec says such a route will simply not be accepted, and the failure surfaces as a condition on the route’s status rather than as silent misbehaviour.

TCPRoute and UDPRoute are deliberately featureless

A TCPRoute may contain exactly one rule, and that rule may define between one and sixteen backendRefs. A UDPRoute has the same shape. There is no matches array, no filters, no hostnames. This is not an oversight to be fixed in v1.7 — it is the honest consequence of the transport layer. A TCP segment carries a five-tuple and nothing else the proxy can key on without parsing the payload.

Weighting exists but is worth reading carefully. When multiple backendRefs are listed, connections are distributed according to weight, and for TCPRoute the unit of weighting is a single new connection — one SYN, one decision. Weight support is classified as Extended for both TCPRoute and UDPRoute, meaning a conformant controller is permitted not to implement it. If you are planning a weighted cutover between two broker clusters, verify weight support in your controller’s conformance report rather than assuming it.

Failure semantics are specified rather than left to implementations. If a backendRef is invalid — it points at a nonexistent object, or at a Service with no endpoints — a TCPRoute implementation MUST actively reject connections to that backend, respecting weight. UDPRoute’s equivalent is that the implementation MUST actively drop datagrams destined for that backend. That wording matters operationally: with a 30 percent weight on a dead backend you get 30 percent hard connection refusals, not 100 percent success through the healthy backend. The route does not silently fail over for you.

Finally, TCP and UDP are distinct protocols as far as listener conflict detection is concerned. An implementation MAY support a TCP listener and a UDP listener on the same port without conflict, which is precisely what DNS on port 53 needs — UDP for normal queries, TCP for zone transfers and responses over 512 bytes, routed by a UDPRoute and a TCPRoute respectively on one Gateway.

SNI routing lives in TLSRoute, not TCPRoute

This is the single most common misunderstanding, and getting it wrong wastes a day. TCPRoute cannot do hostname routing. If you want many logical services behind one port and one IP, distinguished by name, you need TLSRoute — which has been on the Standard channel since v1.5.0 and is likewise gateway.networking.k8s.io/v1.

TLSRoute matches on the SNI hostname presented in the TLS ClientHello. Unlike HTTPRoute and GRPCRoute, its hostnames field is required, the hostname must be a fully qualified domain name, and IPv4 or IPv6 literals are not permitted — a direct consequence of the SNI specification in RFC 6066. The listener carries a mandatory tls.mode of either Passthrough, where the encrypted stream is forwarded untouched, or Terminate, where the Gateway decrypts and forwards a plain TCP stream to the backend.

Three separate conformance features describe this surface. TLSRoute means the implementation supports Passthrough mode and must be reported by anything claiming TLSRoute support at all. TLSRouteModeTerminate means Terminate is also supported. TLSRouteModeMixed means two TLS listeners with different modes can coexist on the same port. Do not assume all three; a controller may report only the first.

Gateway API v1.6 also raised TLSRoute’s CRD validation limits to allow up to 1024 hostnames and rules per resource. The release notes attach an unusually blunt caveat to that change: operators must validate kube-apiserver, etcd and Gateway controller behaviour with representative manifests before enabling the new limit in production. A thousand-hostname TLSRoute is a large object being watched by every controller replica, and the upstream project is telling you to load-test it rather than trust the validation ceiling.

The Three-Way Comparison

Here is the direct answer for the decision most teams are actually making. Use TCPRoute or UDPRoute when you need portable, namespaced, status-reporting L4 routing and your controller passes the v1.6 conformance profile. Use Service type=LoadBalancer when you need a dedicated IP, a protocol your Gateway controller does not proxy, or the absolute minimum of moving parts. Use a vendor CRD only for a feature Gateway API has not standardised yet.

Decision flow for choosing between TLSRoute, TCPRoute, UDPRoute, LoadBalancer Services and vendor CRDs

Figure 2: The decision turns on whether SNI is available, whether you need per-request routing, and whether your controller’s conformance report actually covers L4.

The flow deliberately routes every Gateway API branch through a conformance check before it terminates. Standard-channel status is a statement about the API, not about your cluster. If the answer at that node is no, you fall back to a LoadBalancer Service or a vendor CRD — and that is a legitimate outcome, not a failure.

Dimension Gateway API L4 (v1.6) Service type=LoadBalancer Vendor CRD
Portability gateway.networking.k8s.io/v1, conformance-tested Core API, but behaviour lives in cloud-specific annotations None — rewrite on controller change
SNI routing TLSRoute only; required hostnames field Not possible Usually yes (e.g. Traefik HostSNI)
TLS passthrough tls.mode: Passthrough on the listener Implicit — LB does not decrypt Usually yes
TLS termination at edge tls.mode: Terminate, gated by TLSRouteModeTerminate Only with cloud LB TLS features Usually yes
IPs consumed One per Gateway One per Service One per proxy deployment
Per-listener policy allowedRoutes, kinds and namespaces, plus policy attachment Annotations only Vendor-specific
RBAC split Native — Gateway and Route are separate objects None — one object, one owner Partial
Status reporting Accepted, ResolvedRefs, Programmed conditions Service status only Vendor-specific
Weighted backends Extended support, up to 16 backendRefs No Usually yes

Portability is the point, and it is now testable

The reason this graduation matters more than a typical API promotion is the conformance suite that came with it. Gateway API v1.6 added conformance tests for UDPRoute under GEP-2645, a new GATEWAY-UDP conformance profile, a SupportTCPRoute feature flag, and a TCP/UDP echo server in the conformance harness gated behind a UDP_ECHO_SERVER toggle. TCPRoute conformance tests landed for GEP-2644. An implementation claiming Standard support now has to pass these, which was never true while the types lived in Experimental.

That is what converts “both controllers have a TCPRoute CRD” into “these manifests behave the same on both controllers”. It is also why the honest answer to “is this portable yet” is check the report, and why Figure 2 puts that check on the critical path.

TLS: where you can route by name, and where you cannot

At L7 you route on host, path, header and method. At L4 you get the five-tuple. The only exception is TLS, where the ClientHello leaks the intended hostname in cleartext before the handshake completes, and that single field is what makes multi-tenancy on one port possible.

So the practical rule is: if the protocol is TLS-wrapped and clients send SNI, use TLSRoute in Passthrough mode and you get per-tenant routing with end-to-end encryption and one IP. If the protocol is TLS-wrapped but clients do not send SNI — older embedded MQTT stacks, industrial gateways with hardcoded IP endpoints, some Postgres drivers — SNI routing is unavailable and you are back to one listener per backend. If the protocol is plain TCP, the port is the identifier.

Termination is the other axis. Terminate mode lets the Gateway hold the certificate, which centralises rotation and lets you use a single issuer across every exposed service; see the comparison of Kubernetes pod certificates, SPIFFE/SPIRE and cert-manager for how that interacts with workload identity. Passthrough keeps the certificate on the broker, which is what you want when the backend performs mutual TLS client-certificate authentication — a Gateway that terminates TLS destroys the client certificate the broker needs to authorise the device.

Cost: IP sprawl is a real line item

The arithmetic is unglamorous and usually decisive. A platform exposing MQTT, MQTT over WebSockets, Postgres, a Redis replica and DNS needs five LoadBalancer Services under the old model: five cloud load balancers, five static IPs, five sets of health checks, five DNS records to maintain. On a major cloud, a network load balancer plus a reserved IP is a low-tens-of-dollars monthly item each before traffic, and the operational cost of five DNS records drifting apart is larger than the invoice.

With a Gateway, all five collapse onto one load balancer with one IP and five listeners. In IPv4-constrained environments — on-premises clusters with a small MetalLB pool, or a cloud subscription with a hard quota on reserved addresses — that collapse is not a saving, it is the difference between the design fitting and not fitting.

The counter-argument is real too. One Gateway is one failure domain and one noisy-neighbour domain. A Postgres backup stream saturating the proxy’s connection table affects MQTT on the same Gateway. Splitting into two or three Gateways by criticality tier is usually the right compromise, and it is cheap because the Gateway is just another namespaced object.

The RBAC split is the feature that survives contact with an org chart

Gateway API’s role-oriented model is the part that actually changes how teams work, and it has no equivalent in a LoadBalancer Service.

Ownership split between infra team Gateway resources and app team TCPRoute and TLSRoute objects

Figure 3: The infra team owns GatewayClass and Gateway. Application teams own their own routes in their own namespaces and attach to a listener they cannot modify.

A Service type=LoadBalancer is a single object. Whoever can create it can choose the cloud load balancer type, request a public IP, set externalTrafficPolicy and attach whatever cloud annotations they like. There is no seam to cut along: either an application team can expose things to the internet unsupervised, or they must file a ticket for every change.

Gateway API cuts that object in three. The infra team owns the GatewayClass — which controller, which load balancer class — and the Gateway, which fixes the ports, the TLS mode, the certificates and, crucially, allowedRoutes. Application teams own TCPRoute, TLSRoute and UDPRoute objects in their own namespaces. A tenant team can add a hostname to its own TLSRoute without being able to open a new port, change the TLS mode or touch another tenant’s route. Cross-namespace backend references are gated by ReferenceGrant, which lives in the backend’s namespace, so the team that owns the Service is the team that consents to being routed to. In v1.6 the referencegrant.spec field became required, closing an edge case where an empty grant could be created.

Every attachment attempt produces status. The route reports Accepted and ResolvedRefs; the Gateway reports Programmed and per-listener Accepted with an attachedRoutes count. When a tenant’s route is rejected, the reason is a condition on their own object — BackendNotFound, NotAllowedByListeners, UnsupportedProtocol — rather than a line in a platform-team log they cannot read.

Worked Example: Multi-Tenant MQTT Over TLS

This is the case that justifies the whole exercise for an industrial IoT platform. You run one broker deployment per tenant for blast-radius and data-isolation reasons. Devices connect on 8883 with TLS. You want one public endpoint, per-tenant DNS names, end-to-end encryption, and no per-tenant load balancer.

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: mqtt-edge
  namespace: platform-net
spec:
  gatewayClassName: example-gateway-class
  listeners:
    - name: mqtts
      protocol: TLS
      port: 8883
      tls:
        mode: Passthrough
      allowedRoutes:
        kinds:
          - kind: TLSRoute
        namespaces:
          from: Selector
          selector:
            matchLabels:
              mqtt-tenant: "true"
apiVersion: gateway.networking.k8s.io/v1
kind: TLSRoute
metadata:
  name: tenant-acme
  namespace: tenant-acme
spec:
  parentRefs:
    - name: mqtt-edge
      namespace: platform-net
      sectionName: mqtts
  hostnames:
    - "acme.mqtt.example.com"
  rules:
    - backendRefs:
        - name: mosquitto
          port: 8883

MQTT device connecting through a Gateway that reads SNI and passes the encrypted stream to a per-tenant broker

Figure 4: The Gateway reads SNI from the ClientHello, selects a backend, and opens a second TCP stream. The TLS session itself is never decrypted.

The sequence is worth walking slowly, because two of its properties surprise people. The device opens a TCP connection to port 8883 on the shared load balancer, which hands the stream to the Gateway’s proxy. The device sends a ClientHello containing acme.mqtt.example.com in the SNI extension. The proxy reads that field, matches it against the hostnames of every attached TLSRoute, and selects the tenant’s broker Service. It then opens a new TCP connection to that backend and forwards the encrypted bytes. The TLS handshake completes between the device and the broker; the Gateway never holds a key and never sees a plaintext MQTT packet.

The first surprise is the source IP. Because the proxy terminates the inbound TCP connection and opens a second one, the broker sees the proxy’s address — usually the node IP — rather than the device’s. Cilium’s documentation states this plainly for TLS passthrough: backends see the Envoy IP as the source of forwarded TLS streams. If your broker enforces per-device IP allowlists, or your audit log records client IPs, that logic breaks the day you move behind a Gateway. There is no X-Forwarded-For at L4. The workable answers are to authenticate the device by client certificate or MQTT credentials instead of by address, or to use the PROXY protocol if both your controller and your broker support it — Mosquitto and EMQX both do, but PROXY protocol support is not part of the Gateway API conformance surface, so it is a controller-specific setting.

The second surprise is that mutual TLS survives here and would not under Terminate. Devices presenting client certificates authenticate directly to the broker, which is usually exactly what an industrial deployment wants: the broker’s authorisation rules key on the certificate’s common name, and that identity is preserved end to end.

A few practical notes on this manifest. allowedRoutes.namespaces.from: Selector means only namespaces labelled mqtt-tenant: "true" may attach, so tenant onboarding is a namespace label rather than a Gateway edit. The hostnames field is required and must be an FQDN. And because each tenant’s route matches a distinct hostname, the oldest-route precedence rule discussed below does not bite — hostname matching is what distinguishes TLSRoute from TCPRoute here. If you are tuning the broker side of this design, the MQTT 5 deep dive on shared subscriptions and topic aliases covers what the protocol gives you once the connection lands.

Worked Example: Postgres, and the Limits of a Port

Postgres is the opposite case and shows where L4 routing runs out of road.

apiVersion: gateway.networking.k8s.io/v1
kind: TCPRoute
metadata:
  name: telemetry-db
  namespace: data
spec:
  parentRefs:
    - name: platform-l4
      namespace: platform-net
      sectionName: postgres
  rules:
    - backendRefs:
        - name: pgbouncer
          port: 6432

This works and is a genuine improvement over a dedicated LoadBalancer Service. You get one shared IP, a namespaced object with status, and an allowedRoutes boundary the data team cannot cross.

What you do not get is anything resembling read/write splitting. A TCPRoute cannot see that a connection will issue only SELECT statements, so it cannot send read traffic to replicas. It cannot fail over on the basis of replication lag. It cannot route by database name, because the database name arrives in the startup packet after the connection is established, and TCPRoute does not parse payloads. Every one of those decisions has to live in a connection pooler behind the route — PgBouncer, Pgpool-II or a Postgres-aware proxy — with the TCPRoute pointing at the pooler’s Service.

Postgres also complicates SNI. The sslmode negotiation starts as plaintext: the client sends an SSLRequest packet, the server answers S, and only then does the TLS handshake begin. Many drivers do send SNI once the handshake starts, but not all do, and the extra round trip means a TLS listener has to be tolerant of that pattern. If you want per-cluster Postgres routing by hostname, test SNI emission with your exact driver and version before designing around it. If SNI is absent, one TCP listener per cluster on distinct ports is the reliable design — less elegant, but it works with every client.

Trade-offs, Gotchas, and What Goes Wrong

Two routes on one listener silently pick a winner. Because a TCP or UDP listener has no hostname, SNI or path to discriminate on, attaching multiple TCPRoutes to the same listener results in only one route receiving traffic. All of them report Accepted: True. Precedence follows the general Gateway API rule: the oldest route by metadata.creationTimestamp, with ties broken alphabetically by namespace/name. This is the nastiest failure mode in the whole API, because every status condition is green while half your traffic goes to the wrong place. It is also a live-migration trap — create a new route intending to cut over, and the old one keeps winning because it is older. Delete first, then create, or use a distinct listener.

Standard channel does not mean your controller implements it. This is worth repeating because the release headline invites the opposite conclusion. As of this check in September 2026: Cilium 1.20 supports Gateway API v1.6.1 and passes the Core conformance tests for TCPRoute and UDPRoute alongside HTTPRoute, GRPCRoute, TLSRoute, BackendTLSPolicy, ReferenceGrant and ListenerSet. NGINX Gateway Fabric 2.7 ships v1 TCPRoute and UDPRoute and claims full Gateway API 1.6 conformance, with a later bump to Gateway API 1.6.2. Envoy Gateway supports HTTPRoute, GRPCRoute, TLSRoute, TCPRoute and UDPRoute. Istio’s L4 support has historically been strongest through its own VirtualService tcp block, with TCPRoute support reported for waypoint proxies in ambient mode; I could not confirm a v1.6 conformance report for Istio at the time of writing, so verify it against the upstream conformance reports directory for your exact version rather than taking that as settled. Traefik and Kong both continue to offer their own L4 CRDs. Read the conformance report for the exact version you run.

The CRDs may not be installed. Gateway API ships Standard-channel CRDs as individual YAML files, and controllers do not necessarily require all of them. Cilium’s documentation is explicit that the TCPRoute, UDPRoute and ListenerSet CRDs are optional, and that if they are not installed, Cilium disables support for those features. The symptom is a route object that cannot be created at all, or a controller log line about a missing CustomResourceDefinition — not an obvious “feature disabled” message. Check kubectl get crd | grep -E 'tcproutes|udproutes' before debugging anything else.

Controller-specific modes can exclude L4 entirely. Cilium’s host network mode, which exposes the Gateway directly on the node network instead of through a LoadBalancer Service, is documented as not compatible with TCPRoute and UDPRoute: their traffic bypasses Envoy and reaches the Gateway’s generated Service directly, and host network mode converts that Service to NodePort, so the listener lands on a random port from the node port range rather than the one you configured. That is a design-level incompatibility, not a bug, and it is the kind of detail that only appears in the controller’s docs — never in the API spec.

You lose everything L7 gave you. No per-request routing, no header-based canary, no retries, no request mirroring, no timeouts expressed in request terms, no per-request metrics. Observability collapses to connection counts, bytes and durations. You cannot answer “which tenant is generating the errors” from Gateway telemetry alone, because at L4 there are no errors — only connections that closed. If you need any of that for a TLS-wrapped protocol, the answer is not a better L4 route; it is a protocol-aware proxy or a mesh sidecar that can parse the protocol after decryption, and you should expect to pay for it in latency and operational surface.

v1alpha2 is deprecated and on a clock. Both TCPRoute and UDPRoute retain v1alpha2 in v1.6, but the release notes say it is deprecated and will be removed in a future release. Any manifest, Helm chart or Kustomize base still writing gateway.networking.k8s.io/v1alpha2 should be moved to v1 now, while both are served. A separate but related change: experimental kinds now live in the gateway.networking.x-k8s.io group with an X prefix — XListenerSet, XBackendTrafficPolicy, XBackend. That convention began in v1.3.0 rather than v1.6, so the group is not new, but the boundary it draws is worth internalising: graduating to Standard means being recreated under the gateway.networking.k8s.io group without the prefix, which is a migration, not an upgrade.

Practical Recommendations

Start by reading your controller’s conformance report for the exact version you run, not the project’s marketing page. Confirm that SupportTCPRoute and the GATEWAY-UDP profile appear, and check whether weight is supported before you design a weighted cutover.

Design the listener layout before the routes. Decide which services share a Gateway and which get their own, using blast radius rather than tidiness as the criterion. Name every listener descriptively and bind routes with sectionName, never port, so ports stay an infra-team concern.

Reach for TLSRoute whenever clients send SNI. It is the only way to get many backends behind one port, and Passthrough mode preserves mutual TLS end to end. Test SNI emission with your real client library before committing to the design — embedded MQTT stacks and older database drivers are the usual offenders.

A pre-flight checklist:

  • Confirm the tcproutes and udproutes CRDs are actually installed in the cluster.
  • Set allowedRoutes with both kinds and a namespace selector on every listener.
  • Create a ReferenceGrant in the backend namespace for every cross-namespace backendRef.
  • Verify exactly one route attaches per plain TCP or UDP listener; check attachedRoutes in the Gateway’s listener status.
  • Replace any client-IP-based authorisation with certificate or credential authentication before cutting over.
  • Migrate every v1alpha2 L4 manifest to v1 while both versions are still served.
  • Delete the old route before creating its replacement, so oldest-wins precedence does not pin you to the route you are retiring.

Frequently Asked Questions

What is the difference between TCPRoute and TLSRoute?

TCPRoute forwards a raw TCP stream from a listener to backends with no matching at all — the listener’s protocol and port are the only classifiers, and a route carries just parentRefs and up to sixteen backendRefs. TLSRoute matches on the SNI hostname in the TLS ClientHello, so many backends can share one port and IP. TLSRoute requires a hostnames field and a listener tls.mode of Passthrough or Terminate. If your traffic is TLS and clients send SNI, TLSRoute is almost always the right choice.

Does Gateway API v1.6 replace Service type=LoadBalancer?

No. A Gateway is itself usually exposed by a LoadBalancer Service, so the cloud load balancer does not disappear — it is shared. Service type=LoadBalancer remains the right answer when a workload needs its own dedicated IP, when you rely on cloud load balancer features your Gateway controller does not expose, when the protocol is not TCP or UDP, or when you want the smallest possible number of moving parts. The win from Gateway API is consolidation, portability and the role split, not the elimination of cloud load balancers.

Can I route Postgres traffic by database name with TCPRoute?

No. The database name travels in the Postgres startup packet, which arrives after the TCP connection is established, and TCPRoute performs no payload inspection. Routing by database requires a Postgres-aware proxy such as PgBouncer or Pgpool-II sitting behind the TCPRoute, which parses the startup packet and dispatches accordingly. The same limit applies to read/write splitting: a TCPRoute cannot know whether a connection will issue reads or writes.

Which Gateway controllers support TCPRoute and UDPRoute today?

As of September 2026, Cilium 1.20 supports Gateway API v1.6.1 and passes Core conformance for TCPRoute and UDPRoute, though those CRDs are optional and the feature is disabled if they are absent. NGINX Gateway Fabric 2.7 ships v1 TCPRoute and UDPRoute with claimed full 1.6 conformance. Envoy Gateway supports both. Istio’s position for v1.6 was not confirmable at the time of writing. Always check the upstream conformance reports for the specific controller version you run.

Why do two TCPRoutes on the same listener not load balance?

Because a plain TCP listener has no hostname, SNI or path to distinguish connections, so there is nothing to split on. Gateway API resolves the conflict with route precedence: all attached routes report Accepted: True, but only the oldest by metadata.creationTimestamp — ties broken alphabetically by namespace/name — actually receives traffic. To distribute across multiple backends, list them as multiple backendRefs with weights inside a single route instead, and confirm your controller implements weight, which is only Extended support.

Do I still need a service mesh if I use TCPRoute?

Often yes, for different reasons. TCPRoute solves north-south exposure of L4 traffic with a portable API. It does not give you east-west mTLS between services, identity-based authorisation, retries, or per-request telemetry. If you need protocol-aware behaviour for TLS-wrapped traffic — canarying MQTT by client property, or retrying a failed database connection transparently — you need something that decrypts and parses the protocol, which is mesh or proxy territory rather than L4 routing.

Further Reading

By Riju — about

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *