gRPC for service-to-service calls
Open in the interactive app: practice, quiz, mock interview →In one minute
- gRPC is an RPC framework: you define services and messages in a
.protofile and generate typed client stubs and server interfaces in many languages. - It runs over HTTP/2, with each call as a stream, Protobuf-encoded messages, and status carried in HTTP trailers.
- Four call shapes: unary, server streaming, client streaming and bidirectional streaming.
- Deadlines, cancellation and metadata propagate across hops, which is the feature that matters most at scale.
- Long-lived HTTP/2 connections break naive L4 load balancing; you need L7 proxies or client-side balancing.
How it works
The wire
A gRPC call is an HTTP/2 request: POST /package.Service/Method with content-type: application/grpc, headers for metadata (auth tokens, trace ids) and grpc-timeout for the deadline. The body is a sequence of length-prefixed messages: 1 byte compression flag, 4 bytes big-endian length, then the Protobuf bytes. The response sends headers, zero or more messages, and then trailers with grpc-status and grpc-message; the spec requires the status in trailers even when the call succeeds (gRPC HTTP/2 protocol). Because status lives in trailers, a call can stream for an hour and still report an error at the end.
HTTP/2 multiplexes many concurrent streams on one TCP connection, with per-stream flow control. A single connection can carry thousands of in-flight calls, which removes connection setup cost but concentrates load.
Contracts and codegen
The .proto file is the contract. Code generation gives each language a typed stub, so a field rename is a compile error rather than a production incident. Protobuf's evolution rules apply: add fields with new numbers, never reuse or retype them.
Deadlines and cancellation
gRPC sets no deadline by default, so a call with none can wait forever (gRPC deadlines). Each call should carry an absolute deadline. A server that calls downstream services passes the remaining time: the library turns the deadline into a relative timeout with the elapsed time already taken off, which also sidesteps clock skew between machines. Java and Go propagate it automatically; C++ needs you to turn it on. That way an edge request with a 300 ms budget never triggers a backend call that will run for 2 seconds. When a client cancels or the deadline passes, cancellation propagates down the call tree, freeing work that nobody will use. Without deadlines, slow dependencies accumulate stuck requests until threads or memory run out.
Status codes and retries
gRPC has 17 codes of its own, among them UNAVAILABLE (usually transient and worth retrying with backoff, though not always safe for a non-idempotent call) (gRPC status codes), DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED (rate limited), INVALID_ARGUMENT, NOT_FOUND, ABORTED (concurrency conflict), INTERNAL. A service config can declare retry policies per method, with maximum attempts, backoff and retryable codes, plus hedging (sending a second copy after a delay). Each backoff gets ±20% jitter, and a retryThrottling setting acts as a retry budget: each failure spends a token, each success earns back a fraction of one, and retries stop while the bucket is below half full (gRPC retry guide).
Load balancing
An L4 load balancer balances connections. With HTTP/2 a client opens one connection and sends everything over it, so all its traffic hits one backend, and new backends get nothing until clients reconnect. Fixes: an L7 proxy (Envoy, a service mesh) that balances per request, or client-side balancing where the client resolves all backend addresses and spreads calls with round robin or least-request. Servers should also set a maximum connection age: past it, the server sends GOAWAY and lets in-flight calls finish, with ±10% jitter so clients do not all reconnect at once (gRFC A9). A long-lived stream cannot be rebalanced once it has started, so streaming only where it pays is also a balancing decision (gRPC performance guide).
Browsers
Browsers cannot read HTTP/2 trailers through fetch, so they use gRPC-Web, which moves the trailers into the response body as a final message marked by the top bit of the flag byte. A proxy such as Envoy usually translates between the two (gRPC-Web protocol). The other route is a transcoding gateway that exposes the API as JSON.
Down the stack
Layer 1The contract
Every gRPC call ends with exactly one status: a code and an optional message. Messages within one stream arrive in order and intact. A call that outlives its deadline fails with DEADLINE_EXCEEDED on the client, and the server sees its context cancelled. Without a retry policy a call is attempted at most once from the application's view. The library may still retry transparently when it knows the server application never saw the request: as often as needed if the call never left the client, and once if it reached the server's gRPC library but not the handler (gRPC retry guide).
Layer 2The mechanism
On HTTP/2 a call is one stream. The client sends a HEADERS frame with :method POST, :path /pkg.Service/Method, te: trailers, content-type: application/grpc, grpc-timeout and optional grpc-encoding, then DATA frames holding the 5-byte-prefixed messages; one message can span several DATA frames or several messages can share one. The server answers with HEADERS, DATA, and a final HEADERS frame flagged END_STREAM that carries grpc-status. Cancellation is an RST_STREAM frame with error code CANCEL. Flow control uses WINDOW_UPDATE frames from an initial 65,535-byte window (RFC 9113, which replaced RFC 7540 in 2022), which gRPC grows by probing the bandwidth-delay product. Keepalives are PING frames, and a server that wants a client to reconnect elsewhere sends GOAWAY. On the client, a name resolver produces addresses and a load-balancing policy (pick_first, round_robin, or xDS-driven) picks a subchannel per call.
Layer 3The machine
Each HTTP/2 frame has a 9-byte header and HPACK shrinks repeated headers to a few bytes, so a small unary call costs well under 100 bytes of framing. All streams share one TCP connection, so a single lost segment stalls every stream behind it until the retransmit; on Linux the minimum retransmission timeout is 200 ms, which shows up as p99 spikes on lossy links. One busy connection is also served by one socket and often one I/O thread, which caps throughput and is another reason to keep a small pool of connections per backend. Encoding a 1 KB Protobuf message costs on the order of a microsecond. Cloud load balancers and NATs drop idle TCP flows after a timeout, which is why keepalive intervals must be shorter. AWS NLB defaults to 350 seconds; since 2024 TCP listeners can set 60 to 6,000 seconds, while TLS listeners stay fixed at 350 (AWS NLB docs).
Why it's built this way
- Internal services make many small calls, and new TCP and TLS handshakes are expensive, so gRPC multiplexes calls over long-lived HTTP/2 connections.
- Streaming responses only know their outcome at the end, so status goes in trailers rather than the HTTP status line.
- Many languages must agree on bytes and types, so the contract is an IDL with generated code.
- Deep call chains amplify slowness, so deadlines travel with the call and bound total work.
- Long-lived multiplexed connections make connection-level balancing meaningless, so balancing moves into the client or an L7 proxy.
Diagram
sequenceDiagram
participant C as "Client stub"
participant E as "Envoy (L7)"
participant A as "Service A"
participant B as "Service B"
C->>E: HEADERS :path=/orders.Orders/Get, grpc-timeout=300m
C->>E: DATA [0][len=42][protobuf]
E->>A: same stream, picked per request
A->>B: GetPrice, grpc-timeout=250m
B-->>A: DATA + trailers grpc-status=0
A-->>E: DATA [protobuf]
A-->>E: TRAILERS grpc-status=0
E-->>C: response streamWorked example
A checkout service calls inventory, pricing and fraud services, 15,000 calls per second each, with p99 budgets that must fit inside a 400 ms user-facing deadline.
Switching inventory from JSON over HTTP/1.1 to gRPC: payload drops from 1.4 KB to 380 bytes, and serialisation CPU drops by about 4x. With HTTP/1.1, the client kept 600 connections open to avoid head-of-line blocking; with HTTP/2 it keeps 4 per backend host.
Then a problem: the inventory fleet scales from 20 to 30 pods, but the 10 new pods get almost no traffic, because checkout's existing connections still point at the first 20. Setting MAX_CONNECTION_AGE to 5 minutes on servers and using client-side round robin over DNS-discovered addresses spreads load within minutes.
Deadlines: checkout sets 400 ms. It spends 20 ms locally, then calls pricing with the remaining 380 ms. Pricing hits a slow cache and would take 2 seconds, but cancels at 380 ms, returning DEADLINE_EXCEEDED. Without propagation, pricing would have kept working for 1.6 seconds on 15,000 requests per second, 24,000 wasted in-flight requests during the incident.
In the wild
- Spotify, 2022: a managed xDS outage plus a gRPC client bug log users out. On 8 March a Google Cloud Traffic Director outage, combined with a bug in the gRPC client library, broke service discovery for the services that had moved to xDS-based discovery. Users worldwide were logged out and could not log back in for about 2 hours 23 minutes, while services still on Spotify's DNS-based discovery kept working. Spotify moved the affected services back to DNS and added monitoring of discovery health (Spotify).
- Datadog, 2021: an aggressive reconnect setting floods the network on every rollout. Rollouts of a metrics query service caused errors that looked like DNS. The real cause was a custom gRPC reconnect setting: about 900 clients retried a deleted pod's address roughly every 300 ms, up to 30,000 SYNs per second, and one stale IP drew about 90,000 connection attempts in 90 seconds, filling the AWS VPC connection-tracking table. Restoring the default gRPC backoff stopped the floods (Datadog).
- Dropbox, 2019: mandatory deadlines in Courier. Dropbox's gRPC-based framework, which carries millions of requests per second between hundreds of services, made deadlines mandatory on every method, set per service or per method, with opt-outs only for rare watch calls. Deadlines propagate through metadata down to the database proxy, which kills a SQL query once its deadline passes. The team says this fixed whole classes of reliability problems (Dropbox).
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| gRPC | Typed contracts, compact, streaming, deadlines | Needs L7 or client-side LB; not browser-native | Internal service-to-service traffic |
| REST/JSON | Universal, cacheable, easy to debug | Larger payloads, weak contracts | Public and browser-facing APIs |
| GraphQL | Client-shaped queries | Server complexity, caching | Aggregation for many UI clients |
| Message queue | Decoupled, buffered | Async, no direct response | Work that need not finish in the request |
Pitfalls in production
- No deadline set. Defaults are often infinite, so a slow backend pins resources forever. Require deadlines in client wrappers and lint for them.
- Connection-level balancing. New pods idle while old ones overload. Use L7 proxies or client-side balancing, plus maximum connection age.
- Retry storms. Every layer makes three attempts on
UNAVAILABLE, amplifying load 27x across three hops; attempts multiply layer by layer (Google SRE book), and AWS cites 243x for five layers (AWS Builders' Library). Retry at one layer and use retry throttling. - Huge messages. The 4 MB default limit fails large responses. Stream or paginate instead of raising the limit.
- Idle connections dropped. NAT and cloud load balancers silently drop idle TCP. Client keepalive is off by default, so configure pings within the provider's idle timeout, but not below the server's permitted interval (5 minutes by default): a client that pings more often is sent GOAWAY with
too_many_pings(gRPC keepalive).
Leadership lens
- Own the
.protorepository centrally, with breaking-change checks in CI. - Ask in review: what deadline does this call get, and what happens when it is exceeded?
- Decide one load-balancing strategy across the org (mesh or client library), since mixing causes subtle imbalance.
- Expose a JSON gateway at the edge rather than forcing gRPC onto browser and partner clients.
Check yourself
Why does an L4 load balancer distribute gRPC poorly?
Show answer
It balances TCP connections, and gRPC multiplexes all calls on a few long-lived connections, so load sticks to whichever backends those connections reached.
Why does gRPC put status in trailers rather than the HTTP status code?
Show answer
For streaming calls the outcome is known only at the end, after messages have already been sent.
What does deadline propagation save during an incident?
Show answer
Work that no caller is waiting for; downstream calls stop when the original budget expires instead of piling up.
Sources and further study
Primary
- RFC 9113: HTTP/2: the current HTTP/2 spec (2022), covering streams, flow-control windows, GOAWAY and the frame header.
- gRFC A9: server-side connection management:
MAX_CONNECTION_AGE, idle limits, jitter and why they exist for L4 balancing. - gRPC-Web protocol: how trailers move into the body so browsers can call gRPC.
Systems docs
- gRPC retry guide: retry policy fields, jitter, retry throttling and transparent retries.
- gRPC keepalive guide: client and server keepalive defaults and the
too_many_pingsrule. - gRPC status codes: all 17 codes and how to choose between UNAVAILABLE, ABORTED and FAILED_PRECONDITION.
- AWS Network Load Balancers: idle timeouts that decide your keepalive interval.
Books
- Building Microservices (2nd ed.), Sam Newman, chapter 5 "Implementing Microservice Communication": RPC against REST, serialization formats and handling change between services.
- High Performance Browser Networking, Ilya Grigorik, chapter 12 "HTTP/2" (read free): binary framing, streams and multiplexing underneath gRPC.
- Site Reliability Engineering, chapter 22 "Addressing Cascading Failures" (read free): deadline propagation and why retries at many layers multiply.
Other curricula
- donnemartin/system-design-primer: treats RPC as exposing behaviour for internal calls and REST as exposing data for public APIs, and lists tight coupling and hard debugging as RPC's costs.
- AWS Builders' Library: timeouts, retries and backoff with jitter: frames retries as a load multiplier, retries at one layer only and caps them with a token bucket.
Watch
- gRPC Crash Course (Hussein Nasser): call types, examples, pros and cons.
- gRPC in 5 minutes (gRPC): official short overview from the gRPC team.
Free resources
- gRPC core concepts: call types, deadlines, cancellation and metadata.
- gRPC over HTTP/2 protocol: the exact framing, headers and trailers.
- gRPC deadlines guide: why every call needs one and how they propagate.
- gRPC load balancing: proxy versus client-side balancing.
- gRPC performance best practices: connection reuse, streaming and keepalive advice.