Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

4.5 gRPC: Proxying the Modern RPC

gRPC is HTTP that grew up: binary frames, streams, and trailers over HTTP/2. NGINX speaks it natively — but only if you give it HTTP/2 to speak it on.

Levels: Intermediate ~35 min read

Intermediate

What you will learn

  1. What gRPC actually is, on the wire (method paths, status trailers).

  2. The grpc_pass directive and its three target forms.

  3. The one non-negotiable: HTTP/2 — cleartext (h2c) for the example here, TLS for production.

  4. grpc_set_header, grpc_read_timeout, and the gRPC-specific knobs.

  5. What the Try it yourself exercise proves: full round trips and status codes through the proxy.


1. gRPC in ninety seconds

A gRPC call looks nothing like JSON-over-HTTP and everything like HTTP/2 used properly:

http
POST /echo.EchoService/Echo HTTP/2.0
content-type: application/grpc
te: trailers            ← gRPC status rides HTTP/2 trailers, not a body

<length-prefixed protobuf frames>

The URI path is <package>.<Service>/<Method> — RPC identity baked into the HTTP layer. Responses end with a trailer ( grpc-status: 0) — metadata after the body, an HTTP/2 feature. Two consequences for a proxy: it must speak HTTP/2 end-to-end, and it must not assume the response is over when the body ends.

That's why NGINX has a dedicated grpc_pass rather than reusing proxy_pass: same shape, different engine — it negotiates HTTP/2 upstream, streams bidirectionally, and understands trailers. (A proxy_pass to a gRPC server produces exactly the kind of hang-then-502 you'd expect from speaking HTTP/1.0 at an h2-only listener.)

2. grpc_pass — the three forms

nginx
grpc_pass grpc://127.0.0.1:9040;        # cleartext HTTP/2 upstream
grpc_pass grpcs://10.0.0.5:9443;        # TLS HTTP/2 upstream
# (no unix-socket URI-part games: grpc_pass takes no URI part —
#  the full method path is the route)

The example gateway server, verbatim:

nginx
server {
    listen       8401 http2;              # THE non-negotiable (see §3)
    server_name  grpc.example.test;

    location / {
        grpc_pass  grpc://127.0.0.1:9040;
        grpc_set_header  Host  $host;      # preserve the :authority
    }
}

And the proof — a real gRPC echo through the proxy (via a small protobuf definition and a dependency-light client):

bash
$ python3 client.py 127.0.0.1:8401 hello
echo: hello

$ python3 client.py 127.0.0.1:8401 FAIL
grpc-error INVALID_ARGUMENT        # status codes relayed faithfully

The second call matters more than the first: INVALID_ARGUMENT is a trailer the proxy had to understand and relay for the client to see it. Round-trip bytes prove connectivity; round-trip status proves fidelity.

3. The one non-negotiable: HTTP/2 on the client side

gRPC clients speak HTTP/2 with prior knowledge (cleartext) or over TLS (via ALPN). The listener facing them must therefore be HTTP/2:

nginx
listen 8401 http2;          # cleartext h2
listen 443 ssl http2;       # the production shape (Module 6 adds TLS)

Modern syntax (1.25.1+) separates the concerns:

nginx
listen 8401;
http2 on;                   # same effect, composable with quic (Module 9)

Between NGINX and the backend, grpc_pass negotiates h2 itself ( grpc:// = h2 prior knowledge; grpcs:// = ALPN) — no extra flags.

4. The gRPC-specific knobs

Same families as proxy_pass, gRPC dialect — the ones you'll actually reach for:

Directive

Default

Why you'll touch it

grpc_read_timeout

60s

long-running streams (bi-directional chats, watches) need more

grpc_send_timeout

60s

slow upstream producers

grpc_buffer_size

4k | 8k — response metadata buffer

oversized headers/trailers (big custom metadata) → raise; error mentions it explicitly

grpc_set_header

Host $proxy_host

preserve :authority for vhost-aware backends (our example does this)

grpc_pass_header / grpc_hide_header

—

same response-header shaping as Lesson 4.2

grpc_socket_keepalive

off

TCP keepalives for idle streams through NATs

grpc_intercept_errors

off

treat non-OK grpc-status as HTTP errors for error_page handling

Two NGINX-vs-gRPC translations worth internalizing:

  • Timeouts are per-read, not per-RPC. A stream that exchanges bytes every second stays alive under a 1s grpc_read_timeout even if a single logical RPC runs for hours. Timeouts measure silence, exactly like the WebSocket lesson — the mental model transfers completely.

  • Buffering semantics differ. grpc_buffer_size is for metadata (headers/trailers), not the message stream — messages flow through like WebSocket bytes. Big metadata is the gRPC analog of big headers, and the error message is your friend.

5. gRPC through the front door (production shape)

The h2c listener shown here is a convenience (no PKI on the data path). The production shape stacks this lesson on Module 6:

nginx
server {
    listen 443 ssl;
    http2 on;                       # client side: TLS + ALPN h2
    # …certificates (Module 6)…

    location /echo.EchoService/ {
        grpc_pass grpcs://10.0.0.5:9443;   # upstream TLS, internal CA
    }
    location / {
        grpc_pass grpc://127.0.0.1:9040;   # cleartext INSIDE the trust zone
    }
}

TLS termination at the edge, h2c (or internal mTLS) behind it — the same architecture every HTTP service uses, with grpc_pass as the engine. Module 6's ssl_ curriculum applies unchanged; Module 10's stream mode ( ssl_preread) can even pass gRPC through without terminating TLS when you need SNI-based routing instead.

Try it yourself

  1. Round trip + status (the canonical pair):

bash
   python3 client.py 127.0.0.1:8401 hello
   python3 client.py 127.0.0.1:8401 FAIL
   python3 client.py 127.0.0.1:8401 hi --delay-ms 500
  1. Read the wire yourself. Watch what crosses NGINX:

The access log records the gRPC method path as a plain URI — your Module 12 log formats will treat RPCs as first-class requests.

  1. Service-based routing. Add location = /echo.EchoService/Echo { … grpc_pass grpc://127.0.0.1:9040; } with an add_header X-Route exact — then call Echo and …/Slow and observe which location handled each (the header proves it; the client can't see it, but the access log can — check $upstream_addr and $request).

  2. Break the h2 requirement. Remove http2 from the listen, reload, retry the client — observe the stall, feel the §3 warning in your fingers. Restore.

  3. Timeouts on silence. Set grpc_read_timeout 1s;, call with --delay-ms 3000 — the proxy cuts the silence; the client sees UNAVAILABLE/ DEADLINE semantics. Restore.

Q&A

Q1. Why does proxy_pass to a gRPC backend fail where grpc_pass succeeds?

Answer

proxy_pass speaks HTTP/1.x upstream; gRPC requires HTTP/2 (different framing, bidirectional streams, trailers for grpc-status). grpc_pass negotiates h2 and understands the protocol's shape.

Q2. Where does a gRPC call's error status travel, and why does that matter to a proxy?

Answer

In HTTP/2 trailers ( grpc-status), after the body. A proxy that assumes "headers done, body done, close" would drop or mangle them — the FAIL probe above exists to prove the trailer survives.

Q3. Client errors: streams open but stall immediately, backends healthy, grpc_pass correct. First thing to check?

Answer

The client-facing listen — it must be HTTP/2 ( http2 on; / listen … http2). An h2 client against an h1 listener produces exactly this stall-and-timeout signature.

Q4. Design: route /admin.AdminService/ through an auth check; everything else straight through. Sketch the two locations.

Answer
nginx
location /admin.AdminService/ {
    auth_request /_auth;               # Module 11
    grpc_pass grpc://127.0.0.1:9040;
}
location / {
    grpc_pass grpc://127.0.0.1:9040;
}

— plain location mechanics; the RPC path prefix is the router.

Takeaways

  • gRPC = HTTP/2 with protobuf bodies, method paths as <package>.<Service>/<Method>, and status in trailers.

  • grpc_pass (grpc://, grpcs://) is the engine; the client-facing listener must be HTTP/2 — the one flag everyone forgets.

  • Timeouts measure silence per direction ( grpc_read/send_timeout), not RPC duration — streams stay alive while bytes flow.

  • grpc_buffer_size is metadata-sized; message bytes stream like WebSocket pipes.

  • Location blocks are RPC routers — the whole series' toolkit (auth, limits, logging) becomes per-service policy for free.

Was this page helpful?

Mark this learning resource complete to track your learning progress.