Envoy Proxy Testing: Filter Chains, Health Checks, and Rate Limit Validation
Envoy is the data plane proxy behind Istio, AWS App Mesh, and Contour. It handles routing, load balancing, authentication, rate limiting, and observability for your services. When Envoy is misconfigured, the symptoms are subtle — requests fail under specific conditions, rate limits don't enforce correctly, or health checks pass when they should fail. Testing Envoy configurations before they reach production prevents these surprises.
Understanding What to Test
Envoy configuration has three main layers to test:
- Filter chains — how requests are processed (auth, rate limiting, header manipulation)
- Health checks — how Envoy determines upstream health
- Routing and load balancing — how traffic is distributed
Setting Up Envoy for Testing
For isolated Envoy testing, run it as a standalone container:
# docker-compose.yaml for Envoy test environment
services:
envoy:
image: envoyproxy/envoy:v1.28-latest
ports:
- "10000:10000" # listener port
- "9901:9901" # admin port
volumes:
- ./envoy.yaml:/etc/envoy/envoy.yaml
command: envoy -c /etc/envoy/envoy.yaml --log-level debug
upstream:
image: kennethreitz/httpbin
ports:
- "8080:80"The admin port (9901) is critical for testing — it exposes configuration, stats, and health check status.
Testing Filter Chains
Header Manipulation Filter
Test that Envoy adds and strips headers correctly:
ENVOY_URL="http://localhost:10000"
# Verify Envoy adds X-Request-Id header
HEADERS=$(curl -s -D - "$ENVOY_URL/api/test" -o /dev/null)
echo "$HEADERS" | grep -i "x-request-id" || (echo "FAIL: X-Request-Id not added"; exit 1)
echo "PASS: X-Request-Id header present"
# Verify Envoy strips internal headers from client requests
RESPONSE=$(curl -s -H "X-Internal-Token: secret" "$ENVOY_URL/api/echo-headers")
echo "$RESPONSE" | grep -i "x-internal-token" && (echo "FAIL: internal header leaked to upstream"; exit 1)
echo "PASS: internal header stripped"Auth Filter Testing
# Request without token should return 401
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "$ENVOY_URL/api/users")
[ "$RESPONSE" == "401" ] || (echo "FAIL: expected 401 without token, got $RESPONSE"; exit 1)
# Valid token should pass through
VALID_TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $VALID_TOKEN" \
"$ENVOY_URL/api/users")
[ "$RESPONSE" == "200" ] || (echo "FAIL: valid token rejected with $RESPONSE"; exit 1)
echo "PASS: auth filter working"Verifying Filter Configuration via Admin API
The admin API at port 9901 exposes the active configuration:
# Dump active filter chains
curl -s http://localhost:9901/config_dump | \
jq '.configs[] | select(.["@type"] | contains("Listener")) | \
.. | .http_filters? | select(. != null) | [.[].name]'Use this to verify your configuration was actually loaded, not just that requests work.
Testing Health Checks
# Cluster config with health checking
clusters:
- name: upstream_service
health_checks:
- timeout: 1s
interval: 5s
unhealthy_threshold: 2
healthy_threshold: 1
http_health_check:
path: /healthTest the health check behavior:
# Make upstream unhealthy
curl -X POST http://localhost:8080/admin/set-health?status=unhealthy
sleep 15 # wait for 2 consecutive failures (2 * 5s interval + buffer)
# Verify Envoy marks host unhealthy
UNHEALTHY=$(curl -s http://localhost:9901/clusters | grep "upstream_service.*unhealthy")
[ -n "$UNHEALTHY" ] || (echo "FAIL: upstream not marked unhealthy"; exit 1)
echo "PASS: unhealthy upstream detected"
# Restore and verify recovery
curl -X POST http://localhost:8080/admin/set-health?status=healthy
sleep 10
HEALTHY=$(curl -s http://localhost:9901/clusters | grep "upstream_service.*healthy")
[ -n "$HEALTHY" ] || (echo "FAIL: upstream not marked healthy after recovery"; exit 1)
echo "PASS: upstream health recovery detected"Outlier Detection Testing
outlier_detection:
consecutive_5xx: 3
interval: 10s
base_ejection_time: 30s
max_ejection_percent: 50# Trigger 3 consecutive 5xx errors
for i in 1 2 3; do
curl -s -o /dev/null "$ENVOY_URL/force-500"
sleep 1
done
sleep 5
# Verify the host was ejected
EJECTED=$(curl -s http://localhost:9901/clusters | grep "outlier_detection.*ejected")
[ -n "$EJECTED" ] || (echo "FAIL: outlier not ejected after 3 consecutive 5xx"; exit 1)
echo "PASS: outlier ejection worked"
# After ejection time, verify host rejoins
sleep 35
ACTIVE=$(curl -s http://localhost:9901/clusters | grep "upstream_service" | grep -v "ejected")
[ -n "$ACTIVE" ] || (echo "FAIL: host did not rejoin after ejection timeout"; exit 1)
echo "PASS: ejected host rejoined after 30s"Rate Limit Validation
Rate limiting is one of the most common Envoy configurations and one of the most commonly misconfigured.
http_filters:
- name: envoy.filters.http.local_ratelimit
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
stat_prefix: http_local_rate_limiter
token_bucket:
max_tokens: 10
tokens_per_fill: 10
fill_interval: 60s
filter_enabled:
default_value:
numerator: 100
denominator: HUNDRED
filter_enforced:
default_value:
numerator: 100
denominator: HUNDREDTest rate limiting exhaustion:
RATE_LIMITED=0
SUCCESS=0
# Send 15 requests; first 10 should succeed, rest should get 429
for i in $(seq 1 15); do
CODE=$(curl -s -o /dev/null -w "%{http_code}" "$ENVOY_URL/api/data")
if [ "$CODE" == "429" ]; then
((RATE_LIMITED++))
elif [ "$CODE" == "200" ]; then
((SUCCESS++))
fi
done
[ $SUCCESS -eq 10 ] || (echo "FAIL: expected 10 successes, got $SUCCESS"; exit 1)
[ $RATE_LIMITED -eq 5 ] || (echo "FAIL: expected 5 rate limits, got $RATE_LIMITED"; exit 1)
echo "PASS: rate limiting enforced correctly (10 success, 5 rejected)"Verify Rate Limit Headers
HEADERS=$(curl -s -D - -o /dev/null "$ENVOY_URL/api/data")
echo "$HEADERS" | grep -i "x-ratelimit-remaining" || (echo "FAIL: X-RateLimit-Remaining missing"; exit 1)
echo "$HEADERS" | grep -i "x-ratelimit-limit" || (echo "FAIL: X-RateLimit-Limit missing"; exit 1)
# Verify counter decrements across requests
FIRST=$(curl -s -D - -o /dev/null "$ENVOY_URL/api/data" | grep -i "x-ratelimit-remaining" | tr -d '\r' | awk '{print $2}')
SECOND=$(curl -s -D - -o /dev/null "$ENVOY_URL/api/data" | grep -i "x-ratelimit-remaining" | tr -d '\r' | awk '{print $2}')
[ "$SECOND" -lt "$FIRST" ] || (echo "FAIL: rate limit counter not decrementing"; exit 1)
echo "PASS: rate limit counter decrements correctly"Stats-Based Assertions
Envoy's stats endpoint is a goldmine for testing. Every filter action increments counters:
# Check rate limit decisions
curl -s http://localhost:9901/stats | grep "ratelimit.over_limit"
# Check upstream health
curl -s http://localhost:9901/stats | grep "health_check.success"
curl -s http://localhost:9901/stats | grep "health_check.failure"
# Check HTTP filter execution
curl -s http://localhost:9901/stats | grep "http.ingress_http.downstream_rq_total"Assert against these counters before and after test traffic to verify filters are executing correctly.
Static Config Validation
Always validate Envoy config before deployment:
docker run --rm -v $(pwd)/envoy.yaml:/envoy.yaml \
envoyproxy/envoy:v1.28-latest \
envoy --mode validate -c /envoy.yamlExpected output: configuration '/envoy.yaml' OK
Integration Test in CI
# .github/workflows/envoy-tests.yaml
jobs:
test-envoy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Start Envoy + upstream
run: docker-compose up -d && sleep 5
- name: Wait for admin port
run: timeout 30 bash -c 'until curl -s http://localhost:9901/ready; do sleep 1; done'
- name: Run filter chain tests
run: bash tests/envoy/filter-chain.sh
- name: Run rate limit tests
run: bash tests/envoy/rate-limits.sh
- name: Run health check tests
run: bash tests/envoy/health-checks.sh
- name: Collect stats on failure
if: failure()
run: curl -s http://localhost:9901/stats > envoy-stats.txt
- name: Cleanup
if: always()
run: docker-compose downEnvoy configuration is powerful but unforgiving — a wrong value in a filter chain can silently mishandle every request. Testing each layer in isolation, then together, is the only way to trust that your proxy configuration does what you think it does.
Envoy testing covers your proxy layer. For end-to-end testing and 24/7 uptime monitoring of services behind Envoy, HelpMeTest provides behavioral coverage with usage-based pricing ($0.003/run).