Protocol Translation Patterns at the API Gateway

Modern distributed systems are rarely monoglot. A mobile client speaks HTTP/1.1 JSON, an internal analytics pipeline speaks gRPC, a legacy integration layer expects SOAP/XML, and a real-time dashboard holds a long-lived WebSocket. The API gateway sits at the intersection of all of these, and its protocol translation layer is what lets every consumer speak its native dialect while backend services stay decoupled from transport concerns. This page covers the middleware pipeline design, runnable configuration for Kong 3.x and Envoy 1.32+, security context propagation, and the observability plumbing required to operate translation at production scale.

Architectural Baseline

Before implementing any translation, three mental-model shifts are required.

Translation is not proxying. A plain reverse proxy forwards bytes; a translation gateway must parse, validate, remap, and re-encode. This makes the gateway stateful with respect to schema — it must hold or resolve the source and target schema (OpenAPI spec, protobuf descriptor) at request time, not at boot time.

The canonical internal representation matters. Production translation pipelines decode the inbound envelope into a normalised in-memory structure, apply transformation rules, then re-encode for the upstream. Skipping this step and chaining pairwise converters (HTTP to gRPC then gRPC to AMQP) produces an exponentially growing set of code paths and breaks tracing context across the middleware chain.

Protocol mismatch is a schema mismatch in disguise. The hard problems in HTTP/1.1-to-gRPC translation are not byte-level framing; they are field naming conventions (userId vs user_id), null/absent field semantics, streaming vs request-response lifecycle, and deadline representation. Plan for schema governance before writing gateway config.

The diagram below shows how a single inbound request traverses a translation gateway from inbound protocol detection through to upstream dispatch:

Protocol Translation Pipeline Five-stage gateway pipeline: Protocol Decoder, Schema Validator, AuthN/Z Interceptor, Header and Context Translator, Target Encoder and Connection Pool. Each stage connects left-to-right with arrows. Downstream clients enter from the left; upstream services exit to the right. A schema registry annotation below stages 1 and 2 indicates descriptors are resolved at request time. Inbound HTTP/1.1 gRPC-Web WebSocket Protocol Decoder Framing, decompression Canonical IR Schema Validator Protobuf / OpenAPI Field mapping + coerce AuthN/Z Interceptor JWT / mTLS validation RBAC / ABAC eval Header & Context Translator Trace context, deadlines Header remapping Target Encoder & Conn Pool Re-serialize payload Upstream dispatch Upstream gRPC HTTP/2 REST/JSON Schema Registry (Protobuf / OpenAPI) — resolved by stages 1 & 2 at request time, not at gateway boot

Primary Concept: gRPC-to-REST and REST-to-gRPC Translation

This is the most common translation requirement. An external REST consumer must reach an internal gRPC service, or an internal gRPC service must call an external REST API. The gateway must handle protobuf serialization, HTTP/2 framing, status code normalisation, and deadline forwarding in both directions.

Kong 3.x: grpc-gateway plugin

Kong’s grpc-gateway plugin exposes a gRPC service over a REST/JSON interface. The plugin loads the proto descriptor at startup and uses it to transcode HTTP/JSON requests to protobuf frames.

# Kong 3.x — deck state file excerpt
_format_version: "3.0"

services:
  - name: user-grpc-service
    protocol: grpc
    host: user-svc.internal
    port: 50051
    routes:
      - name: user-rest-route
        protocols: [http, https]
        paths: ["/api/v1/users"]
        methods: [GET, POST, PUT, DELETE]

plugins:
  - name: grpc-gateway
    service: user-grpc-service
    config:
      # Path to the compiled FileDescriptorSet (.pb file)
      proto: /etc/kong/protos/user_service.pb

The proto descriptor file must be compiled with --include_imports to embed all dependencies:

# Compile descriptor — pin to protoc 25.x for deterministic output
protoc --proto_path=./protos \
       --descriptor_set_out=/etc/kong/protos/user_service.pb \
       --include_imports \
       user_service.proto

Status code mapping in Kong follows the gRPC to HTTP table by default (INVALID_ARGUMENT maps to 400, NOT_FOUND to 404, UNAVAILABLE to 503), but you can override per-route in the grpc-gateway config using an error_handler Lua block.

For field name translation (userId to user_id), the plugin respects the json_name option in the proto definition. Set it explicitly rather than relying on protoc’s default camelCase conversion:

// user_service.proto — proto3
message GetUserRequest {
  string user_id = 1 [json_name = "userId"];
}

The complete guide — including buffer management, connection draining under high throughput, and protobuf reflection — is covered in depth in Handling gRPC to REST Translation at Scale.

Envoy 1.32+: grpc_json_transcoder filter

Envoy’s grpc_json_transcoder filter provides the same REST-to-gRPC transcoding natively without a plugin layer. Because Envoy manages its own HTTP/2 connection pool per upstream cluster, there is no additional hop for the translation.

# Envoy 1.32+ — static filter chain snippet
http_filters:
  - name: envoy.filters.http.grpc_json_transcoder
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder
      proto_descriptor: /etc/envoy/protos/user_service.pb
      services:
        - user.v1.UserService
      print_options:
        add_whitespace: true
        always_print_primitive_fields: true
        preserve_proto_field_names: false   # enforce camelCase in JSON responses
      convert_grpc_status: true
      request_validation_options:
        reject_unknown_method: true
        reject_unknown_query_parameters: true

  - name: envoy.filters.http.router
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

The corresponding proto must use google.api.http annotations to describe the REST binding:

import "google/api/annotations.proto";

service UserService {
  rpc GetUser (GetUserRequest) returns (User) {
    option (google.api.http) = {
      get: "/api/v1/users/{user_id}"
    };
  }
  rpc CreateUser (CreateUserRequest) returns (User) {
    option (google.api.http) = {
      post: "/api/v1/users"
      body: "*"
    };
  }
}

Deadline propagation across protocols: gRPC uses a grpc-timeout header (e.g., 100m for 100 milliseconds). REST clients typically use x-request-timeout or a custom header. Envoy’s transcoder converts the HTTP-level timeout from the route configuration into a grpc-timeout header automatically. On Kong, you must do this explicitly in a pre-function plugin:

-- Kong 3.x pre-function plugin: map x-request-timeout to grpc-timeout
local timeout_ms = kong.request.get_header("x-request-timeout")
if timeout_ms then
  kong.service.request.set_header("grpc-timeout", timeout_ms .. "m")
end

Secondary Concept: WebSocket Proxying and Protocol Upgrades

WebSocket sessions cross a protocol boundary that most gateway configurations handle poorly. The upgrade handshake (HTTP/1.1 101 Switching Protocols) must pass through the gateway intact, and once the connection upgrades, the gateway must maintain a persistent bidirectional pipe rather than a request-response cycle. This breaks naive connection pool assumptions.

Connection affinity for WebSocket sessions

WebSocket and long-lived server-sent event (SSE) streams require sticky routing, not round-robin. If the gateway rebalances a WebSocket connection mid-session to a different upstream, the application-layer session state is lost. Use consistent hashing on the session identifier rather than the client IP — IPs change under NAT and CGNAT, but the session cookie or token remains stable.

Kong 3.x — WebSocket sticky routing:

# Kong 3.x — WebSocket upstream with hash-based balancing
upstreams:
  - name: ws-backend
    algorithm: consistent-hashing
    hash_on: header
    hash_on_header: "x-session-id"
    hash_fallback: cookie
    hash_fallback_header: "session"
    healthchecks:
      active:
        healthy:
          interval: 10
          successes: 2
        unhealthy:
          interval: 5
          http_failures: 3

services:
  - name: websocket-service
    host: ws-backend
    protocol: ws          # or wss for TLS upstream
    routes:
      - name: ws-route
        protocols: [http, https]   # gateway handles upgrade
        paths: ["/ws"]

Envoy 1.32+ — WebSocket upgrade filter:

# Envoy 1.32+ — allow WebSocket upgrade on a specific route
virtual_hosts:
  - name: ws-vhost
    domains: ["*"]
    routes:
      - match:
          prefix: "/ws"
          headers:
            - name: "Upgrade"
              string_match:
                exact: "websocket"
        route:
          cluster: ws-backend-cluster
          upgrade_configs:
            - upgrade_type: "websocket"
              enabled: true
          timeout: 0s        # disable idle timeout for long-lived connections
          idle_timeout: 900s # but set a hard 15-minute inactivity limit

HTTP/1.1 to HTTP/2 upstream translation

Envoy performs h2c (HTTP/2 cleartext) upstream by default when the upstream http2_protocol_options key is present. Kong requires the service protocol set to http2 or grpcs. The gateway holds a multiplexed HTTP/2 connection pool to each upstream host, meaning thousands of logical streams share a small number of TCP sockets — a significant efficiency gain over HTTP/1.1 connection-per-request pooling.

The risk here is head-of-line blocking at the TCP layer. A single large response body can stall all other streams on the same TCP connection. Mitigate this by setting a reasonable max_concurrent_streams limit (Envoy default: 100) and by enabling SETTINGS_INITIAL_WINDOW_SIZE tuning:

# Envoy 1.32+ — HTTP/2 upstream pool tuning
clusters:
  - name: grpc-backend
    typed_extension_protocol_options:
      envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
        "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
        explicit_http_config:
          http2_protocol_options:
            max_concurrent_streams: 200
            initial_stream_window_size: 65536        # 64 KB
            initial_connection_window_size: 1048576  # 1 MB

Comparative Implementation Table

Gateway Protocol pair Config mechanism Key trade-off
Kong 3.x REST to gRPC grpc-gateway plugin + .pb descriptor Hot-reloadable via Admin API; descriptor reload requires plugin config update
Kong 3.x HTTP/1.1 to HTTP/2 upstream Service protocol: http2 No filter-level stream tuning; relies on Nginx upstream defaults
Kong 3.x HTTP to WebSocket Service protocol: ws; upstream hash balancing Sticky routing via consistent-hashing; no native SSE-specific handling
Envoy 1.32+ REST to gRPC grpc_json_transcoder filter + proto annotations Requires google.api.http annotations baked into proto; tighter coupling to HTTP binding
Envoy 1.32+ HTTP/1.1 to HTTP/2 http2_protocol_options on cluster Fine-grained stream/window tuning; xDS dynamic update without restart
Envoy 1.32+ HTTP to WebSocket upgrade_configs on route Per-route upgrade control; timeout must be manually set to 0s
Tyk 5.x REST to gRPC proxy.transport.tls + grpc listen type Native gRPC listener but no built-in REST transcoding; requires an external transcoding sidecar

Security Context Preservation Across Protocol Boundaries

Protocol translation physically alters the request envelope, which can silently strip authentication headers, mutate TLS termination state, or obscure the original client identity from downstream services. The gateway must explicitly re-inject security context after translation — this is not automatic.

This requirement sits within the broader security boundaries and zero-trust model, where downstream services must never assume the integrity of the upstream transport layer.

Security context mapping by protocol pair

Inbound Auth mechanism Outbound Preservation strategy
HTTP/1.1 REST Bearer JWT in Authorization gRPC Copy Authorization header into gRPC metadata as authorization key
gRPC mTLS client certificate REST/JSON Extract Subject DN from client cert; inject as x-forwarded-client-cert (XFCC) header
WebSocket Session cookie gRPC-Web Map cookie to gRPC metadata session key; enforce SameSite=Strict; Secure on cookie
HTTP/2 JWT in Authorization HTTP/1.1 upstream Pass through Authorization; strip hop-by-hop headers (:method, :path, :scheme)

After translation, the gateway should re-sign outbound service-to-service requests using short-lived tokens (e.g., SPIFFE/SPIRE SVIDs or gateway-issued JWTs scoped to the specific upstream service). Never allow backend services to receive translated requests without an explicit trust assertion from the gateway.

Strip all client-originated headers that could influence routing or authorization on the upstream:

# Kong 3.x — request-transformer plugin: strip dangerous upstream headers
plugins:
  - name: request-transformer
    config:
      remove:
        headers:
          - x-forwarded-for      # re-add from trusted source only
          - x-real-ip
          - x-custom-auth        # any auth header the client could forge
          - x-grpc-status        # gRPC status should come from upstream only

Observability and Telemetry for Translation Layers

Distributed tracing breaks when protocol boundaries are crossed without explicit context propagation. The gateway must extract W3C Trace Context from the inbound protocol and re-inject it in the target format before forwarding upstream.

For gRPC upstreams, traceparent maps to the grpc-trace-bin binary header (base64-encoded). Envoy’s zipkin or opentelemetry tracer handles this automatically. In Kong, use the opentelemetry plugin with propagation.extract_from = {"w3c"} and propagation.inject_into = {"w3c", "b3"}. The request and response transformation layer runs before this injection step, so trace headers set by upstream must not be inadvertently stripped during JSON field mapping.

Key span attributes to attach at the translation stage:

  • translation.protocol_in — e.g., http1.1, grpc-web
  • translation.protocol_out — e.g., grpc, http2
  • translation.schema_version — proto file package + version string
  • translation.duration_ms — time spent in transcoding, excluding upstream RTT

Metric counters to alert on:

  • schema.validation_failures_total — a non-zero rate indicates a proto drift incident or a client sending malformed payloads
  • translation.timeout_total — upstream deadline exceeded before translation completed; indicates a sizing issue
  • ws.connections_active — should not grow unboundedly; a leak here means WebSocket teardown is not cleaning up file descriptors

Set SLO thresholds: p99 transcoding latency below 15 ms for REST-to-gRPC on payloads under 64 KB; below 5 ms for HTTP/1.1-to-HTTP/2 passthrough where no body transformation is required. Sustained breaches above 50 ms p99 indicate connection pool saturation or schema registry resolution latency — check upstream_connect_time metrics first.

Operational Gotchas

1. Trailing metadata frames in gRPC streaming are dropped. gRPC unary responses send grpc-status and grpc-message as HTTP/2 trailers, not headers. HTTP/1.1 has no trailer support by default. When translating a streaming gRPC response to HTTP/1.1, trailers are silently discarded unless you use chunked transfer encoding with TE: trailers declared by the client. Test this explicitly with grpcurl and an HTTP/1.1 proxy in the path.

2. Compression mismatches increase payload size. If the upstream gRPC service sends grpc-encoding: gzip but the REST client does not declare Accept-Encoding: gzip, the gateway must decompress before forwarding — a CPU spike under high load. Disable per-message gRPC compression at the upstream when running behind a translation gateway that handles transport-level TLS compression independently.

3. Deadline and timeout mismatch causes orphaned upstream calls. When an external REST client closes the connection before the gateway receives an upstream response, most gateways cancel the outbound request. However, gRPC streams with grpc-timeout set to a value longer than the gateway’s upstream_connect_timeout may not propagate cancellation (RST_STREAM) correctly. Verify with a deliberate early-disconnect test and confirm the upstream service receives the cancellation signal.

4. Schema drift between proto descriptor and running service. The gateway loads a compiled .pb descriptor at config time. If the upstream service deploys a new proto version without updating the gateway descriptor, field additions are silently dropped (proto3 compatible) but field removals or type changes cause serialization failures that surface as opaque INTERNAL errors. Implement a CI gate that validates the gateway descriptor against the upstream proto registry on every service deployment.

5. WebSocket connections exhaust file descriptor limits. Each WebSocket session holds an open file descriptor on both the gateway and upstream. At 10,000 concurrent sessions, the gateway process will hit the default OS limit (ulimit -n 65535 on most Linux distributions). Increase worker_rlimit_nofile in Nginx-backed gateways and set LimitNOFILE in the systemd unit for Envoy.

6. HTTP/2 ALPN negotiation fails silently for old TLS stacks. If a client’s TLS library does not support ALPN (TLS 1.2 without ALPN extension), the gateway falls back to HTTP/1.1 without warning. The client then sends HTTP/2 frames over an HTTP/1.1 connection, which produces a 400 Bad Request. Add explicit ALPN negotiation logging at DEBUG level during rollout. Consult Gateway Selection Criteria for a capability matrix comparing ALPN support across Kong, Envoy, Tyk, and NGINX.

Production Configuration Checklist

  • Proto descriptor compiled with --include_imports and versioned in the same CI pipeline as the upstream service
  • json_name annotations set on every proto message field; camelCase mapping verified in integration tests
  • grpc-timeout header propagated from REST client timeout header; maximum enforced at gateway level
  • gRPC status to HTTP status mapping table documented and tested for all error paths (400, 401, 403, 404, 429, 500, 503)
  • WebSocket sticky routing configured with session-token-based consistent hashing, not IP hashing
  • HTTP/2 upstream max_concurrent_streams set and load-tested at 120% of expected peak concurrency
  • Security context headers (Authorization, XFCC) explicitly forwarded; client-forgeable headers stripped in request-transformer
  • File descriptor limits raised (LimitNOFILE >= 500000) on gateway host for WebSocket workloads
  • W3C traceparent / tracestate headers mapped to grpc-trace-bin for gRPC upstreams
  • Schema registry alert configured: gateway deployment blocked if descriptor diverges from upstream proto
  • Translation latency SLO defined: p99 <= 15 ms for stateless REST-to-gRPC; <= 5 ms for HTTP/1.1-to-HTTP/2

FAQ

What is the latency overhead of gRPC-to-REST translation at the gateway?

Stateless protobuf-to-JSON transcoding typically adds 2–8 ms per request depending on payload size and schema complexity. Streaming endpoints carry higher overhead because each message frame must be individually transcoded. Using a compiled proto descriptor cache and connection reuse keeps p99 latency below 15 ms for payloads under 64 KB.

How do I propagate mTLS identity across a protocol translation boundary?

The gateway terminates the inbound mTLS session, extracts the client certificate’s Subject and SAN fields, and injects them as downstream HTTP headers — typically x-forwarded-client-cert (XFCC) for Envoy or a custom x-client-cert-dn header for Kong. The downstream service must treat these headers as authoritative only when they arrive from the gateway’s internal network range, never from external callers.

Can a single gateway route both HTTP/1.1 and HTTP/2 on port 443?

Yes. Envoy and Kong both support ALPN negotiation on a single listener: the gateway advertises h2 and http/1.1 in the TLS handshake and selects the filter chain based on the negotiated protocol. The upstream connection pool is independent — the gateway can translate HTTP/1.1 inbound to HTTP/2 upstream transparently.

What happens to gRPC trailers when translating to HTTP/1.1?

gRPC unary responses send grpc-status and grpc-message as HTTP/2 trailers, not headers. HTTP/1.1 has no trailer support by default. When translating a streaming gRPC response to HTTP/1.1, trailers are silently discarded unless you use chunked transfer encoding with TE: trailers declared by the client. Test this path explicitly — the failure mode is silent data loss, not an error response.


Related

Up: API Gateway Fundamentals & Architecture