How to Configure Kubernetes Ingress for Mixed gRPC and HTTP Services
To configure Kubernetes ingress for mixed gRPC and HTTP services, you need two separate Ingress resources: one for gRPC backends annotated with nginx.ingress.kubernetes.io/backend-protocol: "GRPC" and one for your HTTP services. Both can share the same host and TLS certificate, but they must be distinct Ingress objects because the backend protocol annotation applies to the entire Ingress resource, not to individual paths. If you try to mix gRPC and HTTP paths in a single Ingress, you'll silently break one protocol or the other.
The gRPC Ingress Resource
This Ingress handles all gRPC traffic. TLS is mandatory. gRPC clients almost universally expect HTTP/2 over TLS, and the NGINX Ingress Controller only speaks HTTP/2 to backends when you set GRPC or GRPCS. Use GRPC if your backend pods accept plaintext h2c, or GRPCS if they terminate TLS themselves.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: grpc-ingress
annotations:
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/server-snippet: |
grpc_read_timeout 3600s;
grpc_send_timeout 3600s;
spec:
ingressClassName: nginx
tls:
- hosts: ["api.example.com"]
secretName: api-tls-cert
rules:
- host: api.example.com
http:
paths:
- path: /myapp.UserService/
pathType: Prefix
backend:
service:
name: user-grpc-service
port:
number: 50051
The HTTP/REST Ingress Resource
This companion Ingress handles traditional HTTP traffic on the same hostname. The NGINX Ingress Controller merges rules for the same host, so path-based routing works correctly across both resources. You don't need special annotations beyond your standard HTTP configuration.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: http-ingress
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
spec:
ingressClassName: nginx
tls:
- hosts: ["api.example.com"]
secretName: api-tls-cert
rules:
- host: api.example.com
http:
paths:
- path: /api/
pathType: Prefix
backend:
service:
name: rest-api-service
port:
number: 8080
Why Two Ingress Resources Are Required
The backend-protocol annotation is scoped to the entire Ingress object. When you set it to GRPC, NGINX proxies every backend in that Ingress using HTTP/2 with the gRPC protocol. If you place an HTTP service in the same Ingress, NGINX attempts to send gRPC frames to it and fails with opaque 502 errors. This is the single most common mistake in mixed-protocol setups.
gRPC paths follow the pattern /package.ServiceName/MethodName. NGINX matches these as regular URL paths, which is why Prefix path matching works. You can route different gRPC services to different backends by specifying their fully qualified service names as path prefixes.
Handling gRPC Streaming and Timeouts
The default NGINX proxy timeout is 60 seconds, which silently kills long-lived gRPC streams. For server-streaming or bidirectional-streaming RPCs, you must increase both the proxy timeouts and the gRPC-specific timeouts. The server-snippet annotation injects the grpc_read_timeout and grpc_send_timeout directives that control how long NGINX waits between messages on a stream.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: grpc-streaming-ingress
annotations:
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
# Keep connections alive for 1 hour for long-lived streams.
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
# Enable HTTP/2 preread for multiplexed streams.
nginx.ingress.kubernetes.io/http2-push-preload: "true"
nginx.ingress.kubernetes.io/server-snippet: |
grpc_read_timeout 3600s;
grpc_send_timeout 3600s;
grpc_socket_keepalive on;
spec:
ingressClassName: nginx
tls:
- hosts: ["api.example.com"]
secretName: api-tls-cert
rules:
- host: api.example.com
http:
paths:
- path: /myapp.StreamService/
pathType: Prefix
backend:
service:
name: stream-grpc-service
port:
number: 50052
Backend Service and Health Checks
Your Kubernetes Services for gRPC backends should use ClusterIP with the port your gRPC server listens on. For health checking, gRPC defines a standard health protocol (grpc.health.v1.Health). Configure your pod's readiness probe to use the gRPC probe type that Kubernetes introduced in 1.24+, rather than falling back to an HTTP health endpoint or a TCP check.
apiVersion: v1
kind: Service
metadata:
name: user-grpc-service
spec:
type: ClusterIP
selector:
app: user-grpc
ports:
- name: grpc
port: 50051
targetPort: 50051
protocol: TCP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-grpc
spec:
template:
spec:
containers:
- name: server
ports:
- containerPort: 50051
readinessProbe:
grpc:
port: 50051
initialDelaySeconds: 5
Gotchas and Production Pitfalls
TLS isn't optional for gRPC through NGINX Ingress. Without a TLS block, the ingress controller negotiates HTTP/1.1 and gRPC fails. If you're doing TLS termination at the ingress (the common case), use backend-protocol: GRPC. If your pods also expect TLS, use GRPCS, but that means you're doing double encryption, which you rarely want.
Message size limits will bite you. NGINX defaults to a 1 MB client body size. gRPC messages larger than this return a cryptic RESOURCE_EXHAUSTED or HTTP 413 that surfaces as an UNKNOWN error on the client. Set proxy-body-size to 0 (unlimited) or an explicit limit on the gRPC Ingress.
Load balancing is per-connection, not per-request. HTTP/2 multiplexes many RPCs over a single TCP connection, which means NGINX's default round-robin sends all RPCs from one client to the same pod. For better distribution, set nginx.ingress.kubernetes.io/load-balance: "ewma" or consider a service mesh like Linkerd that does L7 gRPC balancing.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: grpc-ingress-production
annotations:
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
# Remove the body size limit for large protobuf messages.
nginx.ingress.kubernetes.io/proxy-body-size: "0"
# Use EWMA for better per-request distribution over HTTP/2.
nginx.ingress.kubernetes.io/load-balance: "ewma"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/server-snippet: |
grpc_read_timeout 3600s;
grpc_send_timeout 3600s;
spec:
ingressClassName: nginx
tls:
- hosts: ["api.example.com"]
secretName: api-tls-cert
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: grpc-gateway
port:
number: 50051
Alternative: Using Gateway API Instead of Ingress
If you're on Kubernetes 1.27+ and your cluster supports the Gateway API, it's the cleaner long-term approach. Gateway API has first-class support for gRPC routes through the GRPCRoute resource (GA in v1.1.0), which eliminates the need for annotation hacks entirely. You define an HTTPRoute for REST and a GRPCRoute for gRPC, both attached to the same Gateway. This is where the ecosystem is heading, and if you're starting fresh, prefer it over Ingress.
For existing clusters already running NGINX Ingress Controller, the two-Ingress-resource pattern shown above is battle-tested and works reliably in production. Remember the key rules: separate Ingress objects per protocol, TLS always on, timeouts explicitly set, and body size limits removed for gRPC.