gRPC in real systems: contracts, deadlines, and streaming
You usually reach for gRPC when one service needs an answer from another right now, not later.
This isn't a framework tour. It's about the three things that make gRPC worth it in production:
- A typed contract that both sides share, so they don't drift apart.
- Deadlines, so one slow service can't drag down everything that calls it.
- Streaming, for the cases where you need a long-lived connection.
Contents
- The scenario
- gRPC in one minute
- The contract: rules that keep old clients working
- A small TypeScript example with a deadline
- When streaming is the right tool
- What Netflix and Uber learned
- Where teams get hurt
- Checklist
1. The scenario
You run an Order API. Before accepting an order, it has to check stock with an Inventory service:
- The check should take under 50 ms for 95% of requests.
- The data is small and structured: SKU, quantity, and whether it's available.
- A slow Inventory service must not tie up the Order service's workers forever.
This is a synchronous call. While Order waits, the customer waits. That's exactly the kind of call where gRPC is a strong default.
2. gRPC in one minute
gRPC is a way for services to call each other over HTTP/2, using a strongly typed interface written in Protocol Buffers (protobuf).
You define the service once in a .proto file, and then:
- Both sides generate their code from that same file.
- Messages are compact binary protobuf, not JSON text.
- HTTP/2 lets many calls share one connection and compresses headers.
Speed is nice, but it isn't the main benefit. The main benefit is that the contract and every client generated from it stay in sync.
3. The contract: rules that keep old clients working
If you remember one thing from this post, make it this: a .proto file is a public API, so treat it like one.
A few rules keep older clients working as the contract changes:
- Never reuse a field number.
- When you remove a field, mark its number (and name) as
reservedso nobody reuses it by accident. - Add new fields instead of changing what existing ones mean. Renaming is safe on the wire, but changing a field's meaning or type is not.
- Keep messages small and stable.
Here's a contract for the Inventory check:
syntax = "proto3";
package inventory.v1;
service InventoryService {
rpc CheckAvailability(CheckAvailabilityRequest) returns (CheckAvailabilityResponse);
}
message CheckAvailabilityRequest {
string sku = 1;
int32 quantity = 2;
}
message CheckAvailabilityResponse {
bool available = 1;
int32 available_quantity = 2;
}
4. A small TypeScript example with a deadline
This uses @grpc/grpc-js and @grpc/proto-loader.
The server side. The stock number here is a placeholder; a real handler would look it up:
import * as grpc from '@grpc/grpc-js';
function checkAvailability(
call: grpc.ServerUnaryCall<any, any>,
callback: grpc.sendUnaryData<any>,
) {
const { sku, quantity } = call.request;
const inStock = 42; // placeholder: look this up in your inventory store
const available = sku !== '' && quantity > 0 && quantity <= inStock;
callback(null, { available, available_quantity: inStock });
}
The client side, with a deadline:
import * as grpc from '@grpc/grpc-js';
// Give the call a little more room than the p95 target,
// otherwise roughly 1 in 20 normal requests would time out.
const deadline = new Date(Date.now() + 150);
client.CheckAvailability(
{ sku: 'SKU-123', quantity: 2 },
{ deadline },
(err: grpc.ServiceError | null, res?: any) => {
if (err) {
if (err.code === grpc.status.DEADLINE_EXCEEDED) {
// An expected failure: fall back, or tell the user to retry.
}
return;
}
// use res.available
},
);
Deadlines aren't optional. Without them, sooner or later a dependency slows down and requests pile up behind it. And note the number: if the deadline is the same as your p95 target, you'll fail about 5% of perfectly normal calls by design. Set it from your overall latency budget, not your average.
5. When streaming is the right tool
Don't add streaming because it looks advanced. Use it when you actually need a connection that stays open:
- A push connection to mobile apps, with delivery acknowledgements.
- Live updates to a dashboard.
- Sending updates to many clients where polling would waste resources.
If all you need is one request and one response, a normal (unary) call is simpler and much easier to debug.
6. What Netflix and Uber learned
These are very different companies, but they came to similar conclusions.
Netflix: the contract and generated code were the big win
According to the CNCF case study on Netflix, Netflix's platform team evaluated several options and chose gRPC mostly for the interface definition and the code generated from it, which also worked across the languages their engineers were using. At that scale, the savings aren't microseconds per call. They're engineering time and fewer bugs in hand-written clients.
Uber: streaming for mobile push
In Uber's write-up on its next-generation push platform, Uber describes moving its mobile push system to bidirectional gRPC streaming. Keeping messages and their acknowledgements on one stream improved connection latency and made delivery more reliable.
7. Where teams get hurt
These are the problems that show up early.
7.1 No deadlines
Without deadlines, slow calls pile up, retries add even more load, and the system can spiral down. Set a deadline on every call and pass the remaining time along to downstream calls, so a request doesn't keep working after the caller has given up. If you retry, limit the number of retries and back off between them.
7.2 Too much data in one call
gRPC isn't a free pass to send large payloads. Big messages raise tail latency, use more memory, and add garbage-collection pressure in languages like Java, Go, and Node.js. gRPC also caps incoming messages at 4 MB by default. Keep messages small and paginate when you need more.
7.3 Browsers
Browsers can't speak native gRPC the way servers can. If browsers need to call the service, you'll usually put something in front: gRPC-Web through a proxy such as Envoy, or a regular HTTP/JSON API.
7.4 Debugging
With REST, you can curl an endpoint and read the JSON. With gRPC, plan ahead for:
- Structured logs with the method name, status code, and latency.
- Distributed tracing.
- A standard way to call services locally, such as
grpcurlor a small dev client.
8. Checklist
Before choosing gRPC for a service boundary, check that:
- The call is synchronous and has a clear latency budget.
- You'll set deadlines on every call by default.
- You can keep a shared
.protocontract stable and versioned. - You have a plan for observability: status codes, latency, and tracing.
- If browsers need access, you have a plan for a gateway.