← Back to Blog

gRPC Through Proxy in Microservices Architecture: Complete Guide to HTTP/2 Tunneling Setup

A detailed guide on configuring gRPC traffic through proxies in microservices architecture β€” from HTTP/2 tunneling to load balancing and TLS termination.

πŸ“…August 7, 2026
```html

gRPC is a high-performance RPC framework from Google that operates over HTTP/2 and is becoming the de facto standard for inter-service communication. However, as soon as you try to route gRPC traffic through a proxy, issues arise: most traditional proxies do not support HTTP/2 and long-lived streaming connections. In this article, we will explore how to properly configure gRPC through a proxy β€” from choosing the architecture to specific configurations for Nginx, Envoy, and HAProxy.

Why gRPC Works Poorly with Traditional Proxies

To understand the problem, one must delve into how gRPC operates under the hood. The protocol uses HTTP/2 as its transport layer, which fundamentally distinguishes it from conventional REST over HTTP/1.1. Most enterprise proxies, firewalls, and load balancers were designed in the era of HTTP/1.1 and simply cannot handle multiplexed HTTP/2 streams.

Here are specific issues you will encounter when attempting to route gRPC through a traditional HTTP proxy:

  • Downgrade to HTTP/1.1. Many proxies automatically downgrade the protocol version. gRPC requires HTTP/2 β€” without it, the connection simply will not be established, and the client will receive an UNAVAILABLE error.
  • Termination of Long-Lived Connections. gRPC actively uses server and bidirectional streaming. Proxies with aggressive timeouts (especially AWS ELB Classic, some versions of Squid) terminate connections that do not transmit data for longer than 60 seconds.
  • Content-Type Issues. gRPC uses the header Content-Type: application/grpc. Proxies unaware of this type may reject the request or incorrectly buffer the body.
  • Trailers. gRPC uses HTTP/2 trailers to convey the status of the call completion. HTTP/1.1 proxies do not support trailers β€” status information will be lost.
  • Body Buffering. Some proxies buffer the entire response body before sending it to the client. For streaming gRPC calls, this means the client will not receive any messages until the stream is completed.

Key Takeaway:

To operate gRPC through a proxy, a proxy supporting HTTP/2 end-to-end or a special tunneling mode is required. A classic HTTP proxy without configuration adjustments will not suffice.

HTTP/2 Tunneling: How It Works

There are two fundamentally different approaches to proxying gRPC traffic, and it is essential to understand the difference between them to choose the right solution for your architecture.

Approach 1: HTTP/2 End-to-End (Recommended)

The proxy understands HTTP/2 and establishes an HTTP/2 connection with both the client and the backend. It can analyze individual streams, apply request-level load balancing, add headers, and perform TLS termination. This is the most functional option β€” this is how Envoy, Nginx (starting from version 1.13.10), and gRPC-aware load balancers in cloud providers operate.

Approach 2: TCP Tunneling (CONNECT)

The proxy does not analyze HTTP/2 traffic but simply creates a transparent TCP tunnel using the CONNECT method. The client establishes a TLS connection directly with the backend through the tunnel. The proxy only sees the encrypted stream of bytes. This method is simpler to configure but deprives you of the ability to perform load balancing at the gRPC request level and to add headers.

Feature HTTP/2 End-to-End TCP CONNECT Tunnel
Request-Level Load Balancing βœ… Yes ❌ No (only TCP)
TLS Termination at Proxy βœ… Yes ❌ No
Adding Headers βœ… Yes ❌ No
Configuration Complexity Medium Low
Streaming Support βœ… Full βœ… Full
Observability (Metrics) βœ… Detailed ❌ Only TCP

Configuring Nginx as a gRPC Proxy

Nginx has supported gRPC proxying since version 1.13.10 (February 2018). The ngx_http_grpc_module is required for operation and is included in the standard build. Important: Nginx supports HTTP/2 on the client side (frontend), but on the backend, it uses HTTP/2 only for gRPC β€” standard HTTP upstream operates over HTTP/1.1.

Basic gRPC Proxy Configuration on Nginx

server {
    listen 443 ssl http2;
    server_name grpc.example.com;

    # TLS Certificates
    ssl_certificate     /etc/nginx/ssl/server.crt;
    ssl_certificate_key /etc/nginx/ssl/server.key;

    # Modern TLS Parameters
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    location / {
        # Use grpc_pass directive instead of proxy_pass
        grpc_pass grpc://grpc_backend;

        # Timeouts for long-lived streams
        grpc_read_timeout  3600s;
        grpc_send_timeout  3600s;
        grpc_connect_timeout 5s;

        # Pass the real client IP
        grpc_set_header X-Real-IP $remote_addr;
        grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

upstream grpc_backend {
    server backend-1:50051;
    server backend-2:50051;
    server backend-3:50051;

    # Keepalive for HTTP/2 connections
    keepalive 32;
}

Note several critically important details. First, the grpc_pass directive is used instead of proxy_pass β€” these are different modules with different behaviors. Second, the timeouts grpc_read_timeout and grpc_send_timeout are set to 3600 seconds (1 hour) β€” this is important for server streams that may run for long periods without data transmission. Third, keepalive 32 in the upstream allows for the reuse of HTTP/2 connections with backends.

Configuration for Unencrypted gRPC (grpc://)

server {
    listen 80 http2;
    server_name grpc-internal.example.com;

    location / {
        grpc_pass grpc://127.0.0.1:50051;

        # Handling gRPC errors
        error_page 502 = /error502grpc;
    }

    location = /error502grpc {
        internal;
        default_type application/grpc;
        add_header grpc-status 14;
        add_header content-length 0;
        return 204;
    }
}

The error502grpc block is an important detail: when the backend is unavailable, it returns the correct gRPC status UNAVAILABLE (14) instead of HTTP 502, which the gRPC client cannot handle correctly.

Envoy Proxy: The Best Choice for gRPC in Microservices

Envoy was created by Lyft specifically for microservices architecture, and its support for gRPC is implemented at the deepest level. It understands Protocol Buffers, can transcode gRPC to REST, collects detailed metrics for each RPC method, and serves as the foundation for service mesh solutions β€” Istio, AWS App Mesh, and others. If you are building a serious microservices architecture, Envoy is the industry standard.

Basic Envoy Configuration for gRPC

static_resources:
  listeners:
  - name: grpc_listener
    address:
      socket_address:
        address: 0.0.0.0
        port_value: 8080
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          stat_prefix: grpc_proxy
          codec_type: HTTP2
          route_config:
            name: grpc_routes
            virtual_hosts:
            - name: grpc_services
              domains: ["*"]
              routes:
              - match:
                  prefix: "/com.example.UserService"
                route:
                  cluster: user_service
                  timeout: 30s
                  retry_policy:
                    retry_on: "reset,connect-failure,retriable-status-codes"
                    num_retries: 3
                    retriable_status_codes: [14]
              - match:
                  prefix: "/com.example.OrderService"
                route:
                  cluster: order_service
                  timeout: 60s
          http_filters:
          - name: envoy.filters.http.grpc_stats
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_stats.v3.FilterConfig
              emit_filter_state: true
          - name: envoy.filters.http.router
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

  clusters:
  - name: user_service
    connect_timeout: 5s
    type: STRICT_DNS
    lb_policy: ROUND_ROBIN
    http2_protocol_options: {}
    load_assignment:
      cluster_name: user_service
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: user-service
                port_value: 50051

Key advantages of this configuration include routing by gRPC services (the path prefix corresponds to the full service name in the package.ServiceName format), automatic retries on UNAVAILABLE status, and the collection of gRPC metrics through the grpc_stats filter.

gRPC-Web Transcoding in Envoy

One of Envoy's killer features is its built-in gRPC-Web transcoder. Browsers do not support gRPC directly (due to Fetch API limitations on HTTP/2 trailers), so the gRPC-Web protocol is used. Envoy can automatically convert gRPC-Web requests from the browser into standard gRPC for the backend:

http_filters:
- name: envoy.filters.http.grpc_web
  typed_config:
    "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
- name: envoy.filters.http.cors
  typed_config:
    "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.CorsPolicy
- name: envoy.filters.http.router
  typed_config:
    "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

HAProxy and gRPC: Configuration with Load Balancing

HAProxy has supported gRPC since version 1.9.2. It operates at the TCP or HTTP/2 level, can balance gRPC traffic, and perform health checks. HAProxy is a good choice if you are already using it in your infrastructure and want to add gRPC support without introducing a new component.

global
    maxconn 50000
    log stdout format raw local0

defaults
    log global
    timeout connect 5s
    timeout client  3600s
    timeout server  3600s

frontend grpc_frontend
    bind *:443 ssl crt /etc/haproxy/certs/server.pem alpn h2,http/1.1
    mode http
    option http-use-htx
    default_backend grpc_servers

backend grpc_servers
    mode http
    balance leastconn
    option http-use-htx

    # Health check via gRPC Health Checking Protocol
    option httpchk GET /grpc.health.v1.Health/Check
    http-check expect status 200

    server grpc1 10.0.0.1:50051 check ssl verify none
    server grpc2 10.0.0.2:50051 check ssl verify none
    server grpc3 10.0.0.3:50051 check ssl verify none

Note several important settings. alpn h2,http/1.1 in the bind directive indicates that HAProxy accepts both HTTP/2 and HTTP/1.1 connections via TLS ALPN negotiation. timeout client 3600s and timeout server 3600s are critically important parameters for long-lived gRPC streams. The leastconn algorithm is preferable to roundrobin for gRPC, as streaming connections can be long-lived and unevenly load the backends.

TLS Termination and End-to-End Encryption for gRPC

gRPC by default assumes the use of TLS β€” this is part of the specification. In practice, in a microservices architecture, the question arises: where to perform TLS termination and how to organize encryption between components? There are three main patterns.

Pattern 1: TLS Termination at Proxy (Edge TLS)

The proxy accepts encrypted traffic from clients, decrypts it, and forwards it to the backends over an unencrypted channel (or with separate TLS). This is the most common approach in corporate networks. Backends can use grpc.Insecure() for simplified configuration.

Pattern 2: End-to-End Encryption (mTLS)

Mutual TLS (mTLS) is the standard for service mesh. Each service has its own certificate, and both parties verify each other's certificates during each connection. This ensures service authentication and traffic encryption within the cluster. This is how Istio operates with the Envoy sidecar proxy.

# Go: gRPC server with mTLS
import (
    "crypto/tls"
    "crypto/x509"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
)

func createMTLSCredentials() credentials.TransportCredentials {
    cert, _ := tls.LoadX509KeyPair("server.crt", "server.key")
    
    caCert, _ := os.ReadFile("ca.crt")
    caCertPool := x509.NewCertPool()
    caCertPool.AppendCertsFromPEM(caCert)
    
    tlsConfig := &tls.Config{
        Certificates: []tls.Certificate{cert},
        ClientAuth:   tls.RequireAndVerifyClientCert,
        ClientCAs:    caCertPool,
    }
    
    return credentials.NewTLS(tlsConfig)
}

// Create server with mTLS
creds := createMTLSCredentials()
server := grpc.NewServer(grpc.Creds(creds))

Pattern 3: TLS Passthrough

The proxy operates at the TCP level and does not decrypt traffic β€” it simply forwards the encrypted bytes to the backend. This is the simplest option from the proxy's perspective, but it deprives you of the ability to analyze gRPC traffic, add headers, or perform request-level load balancing.

Load Balancing for gRPC Streams: Features and Solutions

Load balancing for gRPC is a non-trivial task, and here's why. In HTTP/1.1, each request is a separate TCP connection (or a connection from a pool), and the load balancer easily distributes requests across backends. In HTTP/2, a single TCP connection multiplexes multiple streams β€” if the load balancer operates at the TCP level, all streams from one connection will hit one backend.

For gRPC, this means: if a client establishes one HTTP/2 connection and sends 100 RPC requests through it, with TCP load balancing, all 100 requests will be handled by one backend instance. The other backends will be idle. The solution is load balancing at the HTTP/2 stream level (L7 load balancing).

Load Balancing Algorithms for gRPC

Algorithm Suitable for gRPC Comment
Round Robin βœ… Yes Good for unary RPCs with approximately equal processing times
Least Connection βœ… Best Choice Considers active streams, evenly distributes load
Random ⚠️ Conditional Works well with a large number of requests, uneven with few
IP Hash ❌ Poor Binds the client to one backend, pointless at L7
Pick First (gRPC built-in) ❌ Not for Production All requests go to the first available server

Client-Side Load Balancing in gRPC

gRPC supports client-side load balancing β€” the client decides which server to send the request to. This allows bypassing the TCP multiplexing issue. For service discovery, DNS with multiple A records or special resolvers (Consul, etcd) is used. Here’s an example of client-side load balancing configuration in Go:

import (
    "google.golang.org/grpc"
    "google.golang.org/grpc/balancer/roundrobin"
)

// Client with round-robin load balancing via DNS
conn, err := grpc.Dial(
    "dns:///grpc-service.internal:50051",
    grpc.WithDefaultServiceConfig(
        `{"loadBalancingConfig": [{"round_robin":{}}]}`
    ),
    grpc.WithTransportCredentials(creds),
)

// Client with least-connection (available in gRPC >= 1.58)
conn, err := grpc.Dial(
    "dns:///grpc-service.internal:50051",
    grpc.WithDefaultServiceConfig(
        `{"loadBalancingConfig": [{"least_request":{}}]}`
    ),
    grpc.WithTransportCredentials(creds),
)

External Proxies for gRPC: When and Why They Are Needed in Microservices

In addition to internal proxy components (Nginx, Envoy, HAProxy), there are times in microservices architecture when it is necessary to use external proxy servers β€” for example, to route traffic through specific regions, bypass network restrictions, or isolate outgoing connections. Let's consider the main scenarios.

Scenario 1: Geo-Distributed Microservices

If your microservices are located in different regions (for example, some in Europe, some in the US), and you need to control which IP address is used for gRPC connections between regions, external proxies can help organize predictable routing. Data center proxies are suitable for such tasks β€” they provide stable IP addresses and high connection speeds, which is critical for gRPC due to its sensitivity to latency.

Scenario 2: Isolation of Outgoing Traffic

In some corporate environments, all outgoing traffic must pass through a corporate proxy. For gRPC clients that need to access external gRPC services (such as Google Cloud APIs that use gRPC), this creates challenges. The solution is to configure a CONNECT tunnel through the corporate proxy.

Here’s an example of configuring a gRPC client to work through an HTTP CONNECT proxy in Go:

import (
    "net"
    "net/http"
    "golang.org/x/net/proxy"
    "google.golang.org/grpc"
)

// Using SOCKS5 proxy for gRPC
proxyDialer, _ := proxy.SOCKS5(
    "tcp",
    "proxy.example.com:1080",
    &proxy.Auth{User: "user", Password: "pass"},
    proxy.Direct,
)

conn, err := grpc.Dial(
    "grpc-service.example.com:443",
    grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
        return proxyDialer.Dial("tcp", addr)
    }),
    grpc.WithTransportCredentials(creds),
)

Scenario 3: Testing and Development

When developing microservices, it is often necessary to test the behavior of services from different network environments β€” to check how a service responds under high latency or to test geo-dependent logic. Residential proxies are convenient for such tasks, allowing you to simulate requests from specific regions with real home user IPs.

Common Issues When Configuring gRPC Through Proxies and Their Resolutions

Let's examine the most common problems developers face when configuring gRPC through proxies and specific ways to resolve them.

Error 1: "transport: received the unexpected content-type"

Symptom:

The client receives an error transport: received the unexpected content-type "text/html; charset=utf-8"

Cause: The proxy returned an HTML error page (for example, 502 Bad Gateway) instead of a gRPC response. The gRPC client cannot handle HTML and throws this error.
Solution: Ensure that the backend is accessible. Add gRPC error handling at the proxy level (as shown in the Nginx example above with the error502grpc block).

Error 2: Connection Drops After 60-120 Seconds

Symptom:

Streaming gRPC connections unexpectedly close with the error UNAVAILABLE: transport is closing after about the same time interval.

Cause: The proxy closes idle connections due to a timeout. Classic values: AWS ELB β€” 60 seconds, Nginx by default β€” 60 seconds.
Solution 1: Increase timeouts on the proxy (as shown in the examples above).
Solution 2: Configure keepalive on the gRPC client side:

import "google.golang.org/grpc/keepalive"

kaParams := keepalive.ClientParameters{
    Time:                30 * time.Second, // Ping every 30 seconds
    Timeout:             10 * time.Second, // Wait for a response for 10 seconds
    PermitWithoutStream: true,             // Ping even without active RPCs
}

conn, err := grpc.Dial(
    "grpc-service:50051",
    grpc.WithKeepaliveParams(kaParams),
    grpc.WithTransportCredentials(creds),
)

Error 3: HTTP/2 Not Negotiated (ALPN Failure)

Symptom:

Error transport: failed to dial: context deadline exceeded or no application protocol

Cause: The proxy or intermediary equipment does not support ALPN (Application-Layer Protocol Negotiation) or h2 is not included in the list of supported protocols.
Solution: Ensure that the TLS configuration of the proxy explicitly specifies the h2 protocol: alpn h2,http/1.1 (HAProxy) or listen 443 ssl http2 (Nginx).

Error 4: Streaming Hangs β€” No Data Arrives

Symptom:

The server stream is working on the backend (visible in logs), but the client does not receive messages until the stream is completed.

Cause: The proxy buffers the response and sends it to the client only after completion. This is a typical problem for proxies configured with proxy_buffering on.
Solution for Nginx:

location / {
    grpc_pass grpc://backend;
    
    # Disable buffering for gRPC streams
    grpc_buffer_size 0;
    
    # Or for regular proxy_pass:
    proxy_buffering off;
    proxy_cache off;
}

gRPC Proxy Diagnostic Checklist

βœ… Checklist: Diagnosing gRPC Proxy

  • The proxy supports HTTP/2 (check with curl --http2 -v)
  • ALPN h2 is enabled in the proxy's TLS configurations
  • Client and server timeouts are set to at least 300 seconds
  • Response buffering is disabled for gRPC endpoints
  • gRPC keepalive is configured on the client (Time: 30s, Timeout: 10s)
  • Backend errors are returned as gRPC statuses, not HTTP codes
  • Health check uses gRPC Health Checking Protocol
  • Load balancing operates at L7 (HTTP/2 streams), not L4 (TCP)

Conclusion

Configuring gRPC through a proxy requires an understanding of the key differences between HTTP/2 and HTTP/1.1: multiplexing streams, long-lived connections, HTTP/2 trailers, and ALPN negotiation. Classic HTTP proxies without special configuration do not work with gRPC β€” either an L7 proxy with HTTP/2 support (Nginx 1.13.10+, Envoy, HAProxy 1.9.2+) or TCP CONNECT tunneling is needed.

For production environments, Envoy is recommended β€” it was specifically designed for microservices architecture, has native support for gRPC, can collect detailed metrics for each RPC method, and is the foundation for most service mesh solutions. Nginx is a good choice if you are already using it as an API Gateway and want to add gRPC support without introducing a new component. HAProxy is suitable if performance is critically important and you are already familiar with its configuration.

Regardless of the chosen proxy, three rules remain unchanged: increase timeouts for long-lived streams, disable response buffering, and configure keepalive on the client. These three settings resolve 80% of issues with gRPC through proxies.

If your architecture requires gRPC services to interact across external networks or you need to route traffic through specific regions, consider using data center proxies β€” they provide stable IP addresses, low latency, and high throughput, which is especially important for gRPC with its binary protocol and sensitivity to latency.

```