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.
Intermediate
What you will learn
What gRPC actually is, on the wire (method paths, status trailers).
The
grpc_passdirective and its three target forms.The one non-negotiable: HTTP/2 — cleartext (h2c) for the example here, TLS for production.
grpc_set_header,grpc_read_timeout, and the gRPC-specific knobs.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:
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
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:
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):
$ 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 faithfullyThe 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:
listen 8401 http2; # cleartext h2
listen 443 ssl http2; # the production shape (Module 6 adds TLS)Modern syntax (1.25.1+) separates the concerns:
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 |
|---|---|---|
| 60s | long-running streams (bi-directional chats, watches) need more |
| 60s | slow upstream producers |
| 4k | 8k — response metadata buffer | oversized headers/trailers (big custom metadata) → raise; error mentions it explicitly |
|
| preserve
|
| — | same response-header shaping as Lesson 4.2 |
| off | TCP keepalives for idle streams through NATs |
| off | treat non-OK grpc-status as HTTP errors for
|
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_timeouteven 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_sizeis 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:
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
Round trip + status (the canonical pair):
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 500Read 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.
Service-based routing. Add
location = /echo.EchoService/Echo { … grpc_pass grpc://127.0.0.1:9040; }with anadd_header X-Route exact— then callEchoand…/Slowand observe which location handled each (the header proves it; the client can't see it, but the access log can — check$upstream_addrand$request).Break the h2 requirement. Remove
http2from thelisten, reload, retry the client — observe the stall, feel the §3 warning in your fingers. Restore.Timeouts on silence. Set
grpc_read_timeout 1s;, call with--delay-ms 3000— the proxy cuts the silence; the client seesUNAVAILABLE/DEADLINEsemantics. 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
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_sizeis 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.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.