Introduction
When you first deploy a service on Kubernetes, it is usually a plain HTTP/1.1 REST API. But as the service grows, things get complicated fast. You switch inter-service communication to gRPC, open a WebSocket for real-time notifications, enable HTTP/2 multiplexing for frontend performance, and start evaluating HTTP/3 (QUIC) to cut latency on mobile networks. All of this traffic must ultimately pass through the cluster's front door: the Ingress controller.
The catch is that the `Ingress` resource itself is fundamentally an abstraction from the HTTP/1.1 era. The `Ingress` API is a simple model that maps hosts and paths to backend Services, and it has no fields that directly express concepts like gRPC streaming, WebSocket upgrades, or QUIC. As a result, these advanced protocols are almost always enabled indirectly through controller-specific annotations or CRDs. The same capability is configured completely differently across ingress-nginx, Traefik, HAProxy, Contour, and Kong.
In this article we walk through the four protocols one by one: what each demands of the Ingress layer, how to configure it per controller, and which pitfalls are easy to fall into, all with hands-on YAML. As of 2026, the `Ingress` API is frozen, meaning no new features are being added, and these advanced protocols are treated as first-class citizens in the successor standard, the Gateway API. So at the end we also look at how the same problems are solved there.
Per-protocol requirements overview
Configuration becomes much clearer once you state exactly what each protocol demands of the Ingress layer.
| Protocol | Core requirement | What the Ingress layer must do |
| --- | --- | --- |
| gRPC | Runs over HTTP/2, bidirectional streaming, trailers | Keep HTTP/2 (h2 or h2c) to the backend, disable buffering |
| WebSocket | HTTP/1.1 Upgrade handshake, long-lived connections | Forward Connection/Upgrade headers, long idle timeouts |
| HTTP/2 | Multiplexing, header compression, TLS ALPN | Client-side h2 negotiation, optionally backend h2 |
| HTTP/3 | QUIC (UDP 443), TLS 1.3 required, Alt-Svc advertise | UDP 443 listener, Alt-Svc header, guaranteed fallback |
Two things stand out. First, with gRPC and HTTP/2 the connection itself must be HTTP/2. Second, WebSocket and HTTP/3 step outside the ordinary request-response model with long-lived connections or a different transport layer. The Ingress controller has to absorb these differences.
[ Client ]
|
TLS 1.3 + ALPN negotiation (h2 / h3 / http/1.1)
|
[ Ingress Controller ]
+-----------+-----------+-----------+
| | | |
gRPC WebSocket HTTP/2 HTTP/3
(h2c?) (Upgrade) (backend) (QUIC->TCP fallback)
| | | |
[ Backend Services ]
gRPC routing
What gRPC demands of Ingress
gRPC runs on top of HTTP/2 framing, so it will not work if the Ingress simply proxies to the backend over HTTP/1.1. The controller must keep the connection to the backend on HTTP/2, disable response buffering, and forward trailers verbatim (gRPC sends `grpc-status` as a trailer). When the backend speaks plaintext HTTP/2 without TLS, that is called h2c (HTTP/2 cleartext).
gRPC on ingress-nginx
ingress-nginx selects the backend protocol with the `nginx.ingress.kubernetes.io/backend-protocol` annotation. For gRPC the value is `GRPC`, or `GRPCS` for a TLS backend.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: grpc-ingress
namespace: demo
annotations:
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
spec:
ingressClassName: nginx
tls:
- hosts:
- grpc.example.com
secretName: grpc-tls
rules:
- host: grpc.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: grpc-server
port:
number: 50051
One thing to note: gRPC over HTTP/2 strongly favors TLS in practice. To negotiate `h2`, the client needs ALPN, and ALPN happens during the TLS handshake. So you always include a `tls` block, as in the example above.
Path-based gRPC method routing
gRPC methods map to HTTP/2 paths. For example, the `helloworld.Greeter/SayHello` method arrives on path `/helloworld.Greeter/SayHello`. So to route per service you can use the package and service name as a path prefix.
- host: grpc.example.com
http:
paths:
- path: /helloworld.Greeter
pathType: Prefix
backend:
service:
name: greeter-svc
port:
number: 50051
- path: /inventory.Stock
pathType: Prefix
backend:
service:
name: stock-svc
port:
number: 50051
gRPC on Traefik
Traefik sets the scheme per service for h2c backends. In Kubernetes this is handled on the Service or IngressRoute via an annotation or ServersTransport.
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: grpc-route
namespace: demo
spec:
entryPoints:
- websecure
routes:
- match: Host(`grpc.example.com`)
kind: Rule
services:
- name: grpc-server
port: 50051
scheme: h2c
tls:
secretName: grpc-tls
gRPC troubleshooting — 504 and UNAVAILABLE
When you first put gRPC behind Ingress you commonly hit two symptoms.
- **504 Gateway Timeout**: the backend protocol is set to HTTP/1.1, so the controller cannot understand HTTP/2 frames. Verify that `backend-protocol` is `GRPC`, and that a plaintext backend is configured as h2c.
- **Streaming responses cut off**: proxy buffering is on, so server streams cannot flow chunk by chunk. In ingress-nginx, turn it off with `nginx.ingress.kubernetes.io/proxy-buffering: "off"`.
- **grpc-status disappears**: trailer forwarding is blocked. Modern controllers support this by default, but an older proxy in front may drop trailers.
WebSocket handling
The Upgrade handshake
WebSocket begins on an HTTP/1.1 connection and switches protocol with the `Upgrade: websocket` and `Connection: Upgrade` headers. The Ingress controller must forward these headers to the backend, and after the switch the connection becomes a long-lived bidirectional stream rather than a request-response pair.
Most controllers detect WebSocket upgrades automatically. ingress-nginx handles them by observing the Upgrade header, with no special annotation needed. The real problem is not the handshake but the timeout.
Increasing the idle timeout
The default proxy timeout is usually around 60 seconds. A WebSocket connection that goes quiet for a while hits this timeout and gets dropped. You therefore need to raise the idle timeout substantially.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ws-ingress
namespace: demo
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
spec:
ingressClassName: nginx
rules:
- host: ws.example.com
http:
paths:
- path: /ws
pathType: Prefix
backend:
service:
name: ws-server
port:
number: 8080
In Traefik you adjust timeouts on the EntryPoint or via middleware. WebSocket itself is handled transparently by Traefik, so no separate enabling is required.
WebSocket troubleshooting
- **Connection drops every minute**: the classic symptom of not raising the read/send timeouts shown above.
- **426 Upgrade Required or failed handshake**: an upstream proxy forcing HTTP/2 is ignoring the Upgrade header. WebSocket must flow over the HTTP/1.1 path.
- **Session breaks while bouncing between backends**: without sticky sessions across replicas, the handshake and subsequent frames can land on different Pods. Enable session affinity with `nginx.ingress.kubernetes.io/affinity: "cookie"`.
HTTP/2 backends
HTTP/2 between the client and the Ingress is often negotiated automatically via ALPN once TLS is enabled. The part that needs separate attention is HTTP/2 between the Ingress and the backend. Even without gRPC, if your backend supports HTTP/2 and you want the multiplexing benefit, set the backend protocol to HTTP/2.
In ingress-nginx you use the same `backend-protocol`.
metadata:
annotations:
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
control client-side HTTP/2 with use-http2 etc. in the global ConfigMap
Client-side HTTP/2 is controlled in the global ingress-nginx ConfigMap. It is usually on by default and negotiates `h2` via ALPN on TLS-enabled hosts.
HTTP/3 and QUIC
Why HTTP/3 is different
HTTP/3 uses QUIC, which runs over UDP rather than TCP, as its transport. That means the Ingress controller must open a UDP 443 listener, TLS 1.3 is mandatory, and to coexist with existing TCP-based HTTP/2 it advertises "this service also speaks HTTP/3" via the `Alt-Svc` header. The client connects over HTTP/2 first, and after seeing Alt-Svc, tries HTTP/3 on subsequent connections.
Per-controller HTTP/3 support
As of 2026, HTTP/3 maturity varies widely by controller.
| Controller | HTTP/3 support | Notes |
| --- | --- | --- |
| ingress-nginx | Limited/experimental | Depends on the QUIC module of nginx, affected by maintenance mode |
| Traefik | Supported | Enabled with an experimental flag, http3 set on the EntryPoint |
| HAProxy | Supported | UDP bind in QUIC builds |
| Contour/Envoy | Dataplane support | Envoy supports HTTP/3, exposure path must be configured |
| Cloud LB based | Generally supported | HTTP/3 toggle on the cloud L7 LB |
Enabling HTTP/3 in Traefik
Traefik enables HTTP/3 on the EntryPoint in static config. The example below shows the value structure.
entryPoints:
websecure:
address: ":443"
http3:
advertisedPort: 443
The key point is that UDP 443 must be open on the LoadBalancer Service and on node firewalls. If it is blocked, the client silently falls back to HTTP/2, which produces the common confusion of "HTTP/3 is enabled but never used."
HTTP/3 troubleshooting
- **Always connects over HTTP/2 only**: UDP 443 is blocked on the LB or firewall, or the Alt-Svc header is not advertised.
- **Intermittent connection failures**: a middlebox somewhere on the path blocks UDP, so QUIC fails. TCP fallback must be guaranteed for normal operation in this case.
TLS ALPN negotiation
Most of the protocols above rely on ALPN (Application-Layer Protocol Negotiation). ALPN is a TLS extension where the client and server agree on which application protocol to use during the handshake. Candidates include `h2` (HTTP/2), `http/1.1`, and `h3` (HTTP/3).
ClientHello (ALPN: h3, h2, http/1.1)
-> ServerHello (ALPN selected: h2)
-> connection proceeds over HTTP/2
So to use gRPC, HTTP/2, and HTTP/3 properly, TLS is a prerequisite. While you can leave the backend in plaintext (h2c), the standard is to almost always enable TLS on the client-to-Ingress leg and manage certificates with something like cert-manager.
Consolidated per-controller support table
| Feature | ingress-nginx | Traefik | HAProxy | Contour |
| --- | --- | --- | --- | --- |
| gRPC | backend-protocol GRPC | scheme h2c | Supported | Supported (HTTPProxy) |
| WebSocket | Automatic | Automatic | Automatic | Automatic |
| Backend HTTP/2 | backend-protocol | ServersTransport | Supported | Supported |
| HTTP/3 | Experimental | Supported | Supported | Via Envoy |
| TLS ALPN | Supported | Supported | Supported | Supported |
Handling in the Gateway API
With the `Ingress` API now frozen, the Gateway API is what fundamentally solves the annotation sprawl above. The Gateway API treats protocols as first-class concepts. For instance, gRPC is expressed with a dedicated `GRPCRoute` resource, letting you declare service- and method-level routing without annotation workarounds.
apiVersion: gateway.networking.k8s.io/v1
kind: GRPCRoute
metadata:
name: greeter
namespace: demo
spec:
parentRefs:
- name: prod-gateway
hostnames:
- grpc.example.com
rules:
- matches:
- method:
service: helloworld.Greeter
method: SayHello
backendRefs:
- name: greeter-svc
port: 50051
HTTP/2 and WebSocket flow naturally through `HTTPRoute`, and the Listener's `protocol` and TLS settings determine ALPN negotiation. HTTP/3 support depends on the maturity of each implementation's Gateway support (Envoy, Traefik, and so on). The key benefit is far greater portability, since these are expressed as standard resources instead of per-controller annotations. For a new cluster, when advanced protocols become a requirement, consider the Gateway API first.
Closing thoughts
Handling gRPC, WebSocket, HTTP/2, and HTTP/3 in Ingress ultimately comes down to "pushing non-HTTP/1.1 traffic through an abstraction built around HTTP/1.1." gRPC means keeping HTTP/2 to the backend and disabling buffering; WebSocket means forwarding the Upgrade and using long timeouts; HTTP/2 means ALPN and the backend protocol setting; HTTP/3 means UDP 443, Alt-Svc, and guaranteed fallback.
The same capability is configured differently per controller, which is an operational burden, but the Gateway API is absorbing that fragmentation into standard resources. Even if you run on `Ingress` today, if demand for advanced protocols grows, it is wise to map out a roadmap for a gradual migration to the Gateway API based on GRPCRoute and HTTPRoute.
References
- Kubernetes Ingress official docs: https://kubernetes.io/docs/concepts/services-networking/ingress/
- ingress-nginx gRPC example: https://kubernetes.github.io/ingress-nginx/examples/grpc/
- ingress-nginx annotation reference: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/
- Traefik Kubernetes IngressRoute: https://doc.traefik.io/traefik/routing/providers/kubernetes-crd/
- HAProxy Kubernetes Ingress Controller: https://www.haproxy.com/documentation/kubernetes-ingress/
- Contour HTTPProxy docs: https://projectcontour.io/docs/
- Gateway API GRPCRoute: https://gateway-api.sigs.k8s.io/api-types/grpcroute/
- Gateway API HTTPRoute: https://gateway-api.sigs.k8s.io/api-types/httproute/
- cert-manager docs: https://cert-manager.io/docs/
현재 단락 (1/184)
When you first deploy a service on Kubernetes, it is usually a plain HTTP/1.1 REST API. But as the s...