The Anatomy of Traffic Management: Dissecting HTTP Routing in Envoy Gateway
On this page
Overview
The HTTPRoute resource allows users to configure HTTP routing by matching HTTP traffic and forwarding it to Kubernetes backends. Currently, the only supported backend supported by Envoy Gateway is a Service resource. This task shows how to route traffic based on host, header, and path fields and forward the traffic to different Kubernetes Services.
Diagram

Install the HTTP routing example resources:
# This file contains the base resources that the docs/user/HTTP_ROUTING.md guide relies on.
# This includes a GatewayClass, Gateway, Services and Deployments that are used as backends
# for routing traffic.
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: example-gateway-class
labels:
example: http-routing
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: example-gateway
labels:
example: http-routing
spec:
gatewayClassName: example-gateway-class
listeners:
- name: http
protocol: HTTP
port: 80
---
apiVersion: v1
kind: Service
metadata:
name: example-svc
labels:
example: http-routing
spec:
ports:
- name: http
port: 8080
targetPort: 3000
selector:
app: example-backend
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: example-backend
labels:
app: example-backend
example: http-routing
spec:
replicas: 1
selector:
matchLabels:
app: example-backend
template:
metadata:
labels:
app: example-backend
spec:
containers:
- name: example-backend
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
resources:
requests:
cpu: 10m
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: example-route
labels:
example: http-routing
spec:
parentRefs:
- name: example-gateway
hostnames:
- "example.com"
rules:
- backendRefs:
- name: example-svc
port: 8080
---
apiVersion: v1
kind: Service
metadata:
name: foo-svc
labels:
example: http-routing
spec:
ports:
- name: http
port: 8080
targetPort: 3000
selector:
app: foo-backend
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo-backend
labels:
app: foo-backend
example: http-routing
spec:
replicas: 1
selector:
matchLabels:
app: foo-backend
template:
metadata:
labels:
app: foo-backend
spec:
containers:
- name: foo-backend
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
resources:
requests:
cpu: 10m
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: foo-route
labels:
example: http-routing
spec:
parentRefs:
- name: example-gateway
hostnames:
- "foo.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /login
backendRefs:
- name: foo-svc
port: 8080
---
apiVersion: v1
kind: Service
metadata:
name: bar-svc
labels:
example: http-routing
spec:
ports:
- name: http
port: 8080
targetPort: 3000
selector:
app: bar-backend
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: bar-backend
labels:
app: bar-backend
example: http-routing
spec:
replicas: 1
selector:
matchLabels:
app: bar-backend
template:
metadata:
labels:
app: bar-backend
spec:
containers:
- name: bar-backend
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
resources:
requests:
cpu: 10m
---
apiVersion: v1
kind: Service
metadata:
name: bar-canary-svc
labels:
example: http-routing
spec:
ports:
- name: http
port: 8080
targetPort: 3000
selector:
app: bar-canary-backend
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: bar-canary-backend
labels:
app: bar-canary-backend
example: http-routing
spec:
replicas: 1
selector:
matchLabels:
app: bar-canary-backend
template:
metadata:
labels:
app: bar-canary-backend
spec:
containers:
- name: bar-canary-backend
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
resources:
requests:
cpu: 10m
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: bar-route
labels:
example: http-routing
spec:
parentRefs:
- name: example-gateway
hostnames:
- "bar.example.com"
rules:
- matches:
- headers:
- type: Exact
name: env
value: canary
backendRefs:
- name: bar-canary-svc
port: 8080
- backendRefs:
- name: bar-svc
port: 8080Verification
Check the status of the GatewayClass:
kubectl get gc --selector=example=http-routingThe status should reflect Accepted=True, indicating Envoy Gateway is managing the GatewayClass.
A Gateway represents configuration of infrastructure. When a Gateway is created, Envoy proxy infrastructure is provisioned or configured by Envoy Gateway. The gatewayClassName defines the name of a GatewayClass used by this Gateway. Check the status of the Gateway:
kubectl get gateways --selector=example=http-routingThe status should reflect “Ready=True”, indicating the Envoy proxy infrastructure has been provisioned. The status also provides the address of the Gateway. This address is used later to test connectivity to proxied backend services.
Testing the Configuration
Without Load Balancer
Get the name of the Envoy service created the by the example Gateway:
export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=example-gateway -o jsonpath='{.items[0].metadata.name}')Port forward to the Envoy service:
kubectl -n envoy-gateway-system port-forward service/${ENVOY_SERVICE} 8888:80 &Test HTTP Routing to the example.com app through Envoy proxy
curl -vvv --header "Host: example.com" "http://localhost:8888/"Output:
* Host localhost:8888 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:8888...
* Connected to localhost (::1) port 8888
> GET / HTTP/1.1
> Host: example.com
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
Handling connection for 8888
< HTTP/1.1 200 OK
< content-type: application/json
< x-content-type-options: nosniff
< date: Tue, 18 Nov 2025 14:02:13 GMT
< content-length: 468
<
{
"path": "/",
"host": "example.com",
"method": "GET",
"proto": "HTTP/1.1",
"headers": {
"Accept": [
"*/*"
],
"User-Agent": [
"curl/8.7.1"
],
"X-Envoy-External-Address": [
"127.0.0.1"
],
"X-Forwarded-For": [
"10.10.0.33"
],
"X-Forwarded-Proto": [
"http"
],
"X-Request-Id": [
"b32842e4-ac2b-47f1-a843-021febe79fa2"
]
},
"namespace": "default",
"ingress": "",
"service": "",
"pod": "example-backend-54988c6bfd-2kl7x"
* Connection #0 to host localhost left intact
}A 200 status code should be returned and the body should include "pod": "example-backend-*" indicating the traffic was routed to the example backend service. If you change the hostname to a hostname not represented in any of the HTTPRoutes, e.g. www.example.com, the HTTP traffic will not be routed and a 404 should be returned.
curl -vvv --header "Host: www.example.com" "http://localhost:8888/"* Host localhost:8888 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:8888...
* Connected to localhost (::1) port 8888
> GET / HTTP/1.1
> Host: www.example.com
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
Handling connection for 8888
< HTTP/1.1 404 Not Found
< date: Tue, 18 Nov 2025 14:17:52 GMT
< content-length: 0
<
* Connection #0 to host localhost left intactThis is because the HTTPRoute you applied before defines example.com not www.example.com.
Test HTTP Routing to the foo.example.com app /login through Envoy proxy
curl -vvv --header "Host: foo.example.com" "http://localhost:8888/login"Output:
* Host localhost:8888 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:8888...
* Connected to localhost (::1) port 8888
> GET /login HTTP/1.1
> Host: foo.example.com
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
Handling connection for 8888
< HTTP/1.1 200 OK
< content-type: application/json
< x-content-type-options: nosniff
< date: Tue, 18 Nov 2025 14:24:38 GMT
< content-length: 473
<
{
"path": "/login",
"host": "foo.example.com",
"method": "GET",
"proto": "HTTP/1.1",
"headers": {
"Accept": [
"*/*"
],
"User-Agent": [
"curl/8.7.1"
],
"X-Envoy-External-Address": [
"127.0.0.1"
],
"X-Forwarded-For": [
"10.10.0.33"
],
"X-Forwarded-Proto": [
"http"
],
"X-Request-Id": [
"1a02d909-a2e1-4563-ba5b-7d9495f7372f"
]
},
"namespace": "default",
"ingress": "",
"service": "",
"pod": "foo-backend-758cc5c78b-gb9ld"
* Connection #0 to host localhost left intact
}A 200 status code should be returned and the body should include “pod”: “foo-backend-*” indicating the traffic was routed to the foo backend service. Traffic to any other paths that do not begin with /login will not be matched by this HTTPRoute. Test this by removing /login from the request.
Test HTTP routing to the bar-canary-svc backend by adding the env: canary header to the request.
curl -vvv --header "Host: bar.example.com" --header "env: canary" "http://localhost:8888/"Output:
* Host localhost:8888 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:8888...
* Connected to localhost (::1) port 8888
> GET / HTTP/1.1
> Host: bar.example.com
> User-Agent: curl/8.7.1
> Accept: */*
> env: canary
>
* Request completely sent off
Handling connection for 8888
< HTTP/1.1 200 OK
< content-type: application/json
< x-content-type-options: nosniff
< date: Tue, 18 Nov 2025 14:28:09 GMT
< content-length: 502
<
{
"path": "/",
"host": "bar.example.com",
"method": "GET",
"proto": "HTTP/1.1",
"headers": {
"Accept": [
"*/*"
],
"Env": [
"canary"
],
"User-Agent": [
"curl/8.7.1"
],
"X-Envoy-External-Address": [
"127.0.0.1"
],
"X-Forwarded-For": [
"10.10.0.33"
],
"X-Forwarded-Proto": [
"http"
],
"X-Request-Id": [
"132234dd-eeb5-417c-8abd-97db06ab4d21"
]
},
"namespace": "default",
"ingress": "",
"service": "",
"pod": "bar-canary-backend-56c4c6b46-zm6sd"
* Connection #0 to host localhost left intact
}A 200 status code should be returned and the body should include “pod”: “bar-canary-backend-*” indicating the traffic was routed to the foo backend service.
Manage Canary Traffic
We could also manage the traffic by percentage to the specific resource (e.g., Pod)
Update the HTTP Route for bar-route, which becomes:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: bar-route
labels:
example: http-routing
spec:
parentRefs:
- name: example-gateway
hostnames:
- "bar.example.com"
rules:
- backendRefs:
- name: bar-canary-svc
port: 8080
weight: 90 # <--- 90% of the total traffic will be redirected to the bar-canary-svc
- name: bar-svc
port: 8080
weight: 10 # <--- 10% of the total traffic will be redirected to the bar-svcSimulate the Traffic
You could create a sample script to check which service the traffic will be redirected to
#!/bin/bash
HOST="bar.example.com"
TARGET_URL="http://localhost:8888/"
REQUEST_COUNT=5
CANARY_PREFIX="canary"
echo "--- Starting 90/10 Canary Weight Test ---"
echo "Sending $REQUEST_COUNT requests to Host: $HOST (via $TARGET_URL)"
echo "------------------------------------------"
CANARY_COUNT=0
STABLE_COUNT=0
UNMATCHED_COUNT=0
for i in $(seq 1 $REQUEST_COUNT); do
# Send request and extract the pod name using the user-defined pipeline
# -sS suppresses progress but shows error if failed
POD_NAME=$(curl -sS --header "Host: $HOST" "$TARGET_URL" | grep pod | awk -F '"' '{print $4}')
# Check if the extracted pod name contains the defined canary prefix
if [[ "$POD_NAME" == *"$CANARY_PREFIX"* ]]; then
echo "Request $i: Routed to CANARY pod ($POD_NAME)"
CANARY_COUNT=$((CANARY_COUNT + 1))
elif [[ -n "$POD_NAME" ]]; then
# If POD_NAME is not empty and does not contain 'canary', assume it is stable
echo "Request $i: Routed to STABLE pod ($POD_NAME)"
STABLE_COUNT=$((STABLE_COUNT + 1))
else
echo "Request $i: FAILED to get a valid pod name from the response."
UNMATCHED_COUNT=$((UNMATCHED_COUNT + 1))
fi
done
echo "------------------------------------------"
echo "FINAL RESULTS (90/10 Split Demonstration):"
echo "STABLE Pod Count: $STABLE_COUNT requests"
echo "CANARY Pod Count: $CANARY_COUNT requests"
echo "Failed/Unmatched Count: $UNMATCHED_COUNT requests"
echo ""
echo "Note: For a small number of requests (like $REQUEST_COUNT), deviations from the 90/10 ratio are expected due to probability."Run the script
./test-canary.shOutput:
--- Starting 90/10 Canary Weight Test ---
Sending 5 requests to Host: bar.example.com (via http://localhost:8888/)
------------------------------------------
Request 1: Routed to CANARY pod (bar-canary-backend-56c4c6b46-zm6sd)
Request 2: Routed to CANARY pod (bar-canary-backend-56c4c6b46-zm6sd)
Request 3: Routed to CANARY pod (bar-canary-backend-56c4c6b46-zm6sd)
Request 4: Routed to STABLE pod (bar-backend-7dcd687b74-hmfhz)
Request 5: Routed to CANARY pod (bar-canary-backend-56c4c6b46-zm6sd)
------------------------------------------
FINAL RESULTS (90/10 Split Demonstration):
STABLE Pod Count: 1 requests
CANARY Pod Count: 4 requests
Failed/Unmatched Count: 0 requests
Note: For a small number of requests (like 5), deviations from the 90/10 ratio are expected due to probability.