gRPC
gRPC is a high-performance framework for calling operations on a remote service through a strongly typed contract.
The client calls a generated method, but the implementation runs on another process or machine.
Client calls getUser(id)
|
v
Generated client stub serializes the request
|
v
gRPC transports it over HTTP/2
|
v
Server stub invokes the service method
|
v
Response returns through the same path
One-line idea: gRPC makes remote service communication look like typed method calls while Protocol Buffers define the contract and HTTP/2 carries the messages.
Core gRPC Concepts
Remote Procedure Call
RPC stands for Remote Procedure Call. It lets one program request that another program execute an operation.
const user = await client.getUser({ id: "101" });
This resembles a local function call, but a remote call is fundamentally different:
- data crosses a network
- either process can fail independently
- latency is much higher than an in-memory call
- requests may time out or be cancelled
- retries can repeat an operation
- the client and server can run different versions
A good gRPC API keeps this distributed behavior visible through deadlines, status handling, idempotency and observability.
Main Building Blocks
| Component | Responsibility |
|---|---|
.proto file | Defines services, methods and message types |
| Protocol Buffers | Serialize structured messages into a compact binary format |
| Code generator or runtime loader | Creates language-specific client and server interfaces |
| Client stub | Converts a method call into a gRPC request |
| gRPC runtime | Manages HTTP/2 streams, metadata, deadlines and message framing |
| Server stub | Decodes the request and invokes the registered handler |
| Service implementation | Runs business logic and returns a result or status |
Where gRPC Fits
gRPC is commonly used for:
- internal microservice communication
- low-latency backend calls
- polyglot systems with services written in different languages
- high-volume APIs with strict contracts
- server, client or bidirectional streaming
- communication between mobile or native clients and backend services
It is less natural for a public browser API because browsers do not expose all required HTTP/2 features directly to JavaScript. Browser clients normally use gRPC-Web through a compatible proxy or server.
Request and Response Flow
Suppose a client calls GetUser.
1. Application calls the generated client method
2. Client stub validates and serializes the request
3. gRPC frames the binary message
4. HTTP/2 transports it to the server
5. Server runtime deserializes the request
6. Registered service handler executes
7. Handler returns a response or gRPC status
8. Response is serialized and sent to the client
9. Client stub returns a language-native object
Client Stub
The client stub is generated from the service contract. It knows:
- available method names
- request and response message types
- whether the method is unary or streaming
- how to serialize and deserialize each message
The stub acts like a typed network client. It does not remove the realities of network failure.
Server Stub
The server side exposes generated interfaces or service descriptors. The application registers handlers that implement those methods.
Generated service definition
+
Application handler functions
|
v
Running gRPC server
RPC Runtime
The runtime manages transport details such as:
- HTTP/2 connections and streams
- message framing
- metadata
- compression
- flow control
- deadlines and cancellation
- status codes
- connection security
Protocol Buffers
Protocol Buffers, usually called Protobuf, provide both an interface definition language and a binary serialization format.
Defining a Service Contract
syntax = "proto3";
package users.v1;
service UserService {
rpc GetUser(GetUserRequest) returns (User);
}
message GetUserRequest {
string id = 1;
}
message User {
string id = 1;
string name = 2;
string email = 3;
}
This contract defines:
- the package namespace
- the
UserServiceservice - the
GetUserRPC method - request and response message shapes
- field types and stable numeric tags
Serialization
The sending side converts a language-native object into Protobuf bytes.
{ id: "101" }
|
v
Protobuf serialization
|
v
Compact binary message
The receiver uses the same contract to deserialize the bytes.
Protobuf is often smaller and faster to encode than text JSON, but actual performance depends on message shape, workload, network conditions and implementation.
Field Numbers
The numbers after message fields are wire identifiers, not display order.
string id = 1;
string name = 2;
Once a field is deployed:
- do not change its number
- do not reuse its number for a different meaning
- reserve the name and number if the field is removed
- prefer adding new optional fields for compatible evolution
message User {
reserved 3;
reserved "email";
string id = 1;
string name = 2;
}
Old clients ignore unknown fields, and new clients use default values when older messages do not contain a field. Compatibility still depends on making safe schema changes.
Common Protobuf Types
| Type | Example use |
|---|---|
string | Names and identifiers |
bool | Flags |
int32, int64 | Signed integers |
uint32, uint64 | Unsigned integers |
float, double | Decimal values |
bytes | Raw binary data |
enum | A controlled set of named values |
repeated | A list of values |
| nested message | Structured domain data |
Use well-known types such as timestamps and durations where appropriate instead of inventing incompatible representations.
Generated Code vs Runtime Loading
There are two common approaches:
| Approach | Behavior |
|---|---|
| Generated code | A compiler creates strongly typed source code before the application runs |
| Runtime loading | The application loads .proto definitions and creates service descriptors at runtime |
Generated code usually gives the strongest editor and compile-time support. Runtime loading can be convenient in dynamic languages and small examples.
HTTP/2 Transport
gRPC commonly uses HTTP/2 as its transport.
Multiplexing
HTTP/2 can carry several independent streams over one connection.
One TCP connection
|- RPC stream A
|- RPC stream B
|- RPC stream C
`- RPC stream D
This avoids creating a separate connection for every call and allows concurrent RPCs without application-level request ordering.
Binary Framing
gRPC messages are placed inside HTTP/2 data frames. The receiver reconstructs message boundaries before deserializing Protobuf data.
Header Compression
HTTP/2 compresses repeated header metadata, which reduces overhead across many calls on the same connection.
Flow Control
HTTP/2 provides flow control for the connection and each stream. This helps prevent a fast sender from overwhelming a slower receiver, especially during streaming calls.
Connection Behavior
Long-lived connections improve efficiency, but production systems must manage:
- connection establishment and TLS handshakes
- keepalive settings
- idle connection policies
- connection draining during deployment
- proxy and load balancer compatibility
- maximum concurrent streams
gRPC is not always faster than REST. Small traffic volumes, extra proxies, inefficient handlers or poor connection management can remove its transport advantages.
Four RPC Styles
gRPC supports four communication patterns.
Unary RPC
One request produces one response.
Client ---- one request ----> Server
Client <--- one response ---- Server
rpc GetCustomer(CustomerRequest) returns (Customer);
Use unary calls for normal lookups, commands and CRUD-like operations.
Server-Streaming RPC
One request produces a stream of responses.
Client ---- one request ----> Server
Client <--- response 1 ------ Server
Client <--- response 2 ------ Server
Client <--- response 3 ------ Server
rpc WatchOrders(WatchOrdersRequest) returns (stream OrderUpdate);
Use it for event feeds, progress updates or large result sets that should arrive incrementally.
Client-Streaming RPC
A stream of requests produces one response.
Client ---- message 1 ------> Server
Client ---- message 2 ------> Server
Client ---- message 3 ------> Server
Client <--- final response -- Server
rpc UploadReadings(stream Reading) returns (UploadSummary);
Use it for telemetry, chunked input or client-side aggregation.
Bidirectional-Streaming RPC
Both sides independently send streams of messages.
Client <====================> Server
independent streams
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
Use it for interactive sessions, collaborative features or continuous two-way updates.
Messages remain ordered within each stream, but the application must define its own business ordering and recovery rules.
Metadata
Metadata carries information about a call outside the Protobuf request message. It is similar to HTTP headers.
Common uses include:
- authentication credentials
- tracing identifiers
- request IDs
- locale or client version
- feature flags
authorization: Bearer <token>
x-request-id: 8a8d...
Keep business data in request messages. Use metadata for cross-cutting request context.
Binary metadata keys use a special binary convention. Do not place secrets in logs or return them in error details.
Deadlines and Cancellation
A client should normally set a deadline, which defines the maximum time it will wait for an RPC.
Client starts call with 500 ms deadline
|
v
Server and downstream calls share the remaining time
|
v
Work completes or the call ends with DEADLINE_EXCEEDED
Without deadlines, slow calls can consume connections, memory and worker capacity for too long.
When a client cancels a call or its deadline expires, the server should stop unnecessary work where possible. Cancellation should also propagate to downstream RPCs and database operations.
A deadline is not a guarantee that the server did nothing. The server may have completed a write just before the client observed a timeout.
Status Codes and Errors
gRPC uses a standard status model instead of relying on ordinary REST status handling.
| Status | Typical meaning |
|---|---|
OK | Operation completed successfully |
INVALID_ARGUMENT | Input is malformed regardless of system state |
NOT_FOUND | Requested resource does not exist |
ALREADY_EXISTS | Resource creation conflicts with an existing resource |
UNAUTHENTICATED | Valid authentication credentials are missing |
PERMISSION_DENIED | Caller is authenticated but not allowed |
FAILED_PRECONDITION | System state does not allow the operation |
ABORTED | Operation was aborted, often because of concurrency conflict |
RESOURCE_EXHAUSTED | Quota or capacity limit was exceeded |
DEADLINE_EXCEEDED | Deadline expired before completion |
UNAVAILABLE | Service is temporarily unavailable |
INTERNAL | Unexpected server failure |
Choose the narrowest status that matches the failure. Clients use status codes to decide whether to correct input, authenticate, stop, retry or alert.
Rich error details can provide structured information such as field violations or retry guidance. Keep the public error contract stable and do not expose stack traces.
Retries and Idempotency
Some failures are transient, but retrying every RPC is unsafe.
Client sends CreatePayment
|
Server completes the write
|
Response is lost
|
Blind retry may create a duplicate payment
Retry only when:
- the status is known to be transient
- the retry stays within the original deadline
- backoff and jitter are applied
- the operation is idempotent or has an idempotency key
- retry limits prevent traffic amplification
Read operations are often safe to retry. Mutations require explicit API design.
Hedging can send a second request before the first one finishes, but it should be reserved for safe operations and tightly controlled because it increases load.
Small Node.js Example
Install the packages:
npm install @grpc/grpc-js @grpc/proto-loader
Create customers.proto:
syntax = "proto3";
package customers.v1;
service CustomerService {
rpc GetCustomer(GetCustomerRequest) returns (Customer);
rpc ListCustomers(ListCustomersRequest) returns (ListCustomersResponse);
rpc CreateCustomer(CreateCustomerRequest) returns (Customer);
}
message GetCustomerRequest {
string id = 1;
}
message ListCustomersRequest {}
message ListCustomersResponse {
repeated Customer customers = 1;
}
message CreateCustomerRequest {
string name = 1;
int32 age = 2;
string address = 3;
}
message Customer {
string id = 1;
string name = 2;
int32 age = 3;
string address = 4;
}
Load the contract and register server handlers:
import grpc from "@grpc/grpc-js";
import protoLoader from "@grpc/proto-loader";
import { randomUUID } from "node:crypto";
const definition = protoLoader.loadSync("customers.proto", {
keepCase: true,
longs: String,
enums: String,
defaults: true,
});
const descriptor = grpc.loadPackageDefinition(definition);
const { CustomerService } = descriptor.customers.v1;
const customers = [
{ id: "1", name: "John Doe", age: 30, address: "123 Main St" },
];
const handlers = {
getCustomer(call, callback) {
const customer = customers.find(({ id }) => id === call.request.id);
if (!customer) {
callback({
code: grpc.status.NOT_FOUND,
details: "Customer not found",
});
return;
}
callback(null, customer);
},
listCustomers(_call, callback) {
callback(null, { customers });
},
createCustomer(call, callback) {
const customer = { id: randomUUID(), ...call.request };
customers.push(customer);
callback(null, customer);
},
};
const server = new grpc.Server();
server.addService(CustomerService.service, handlers);
server.bindAsync(
"127.0.0.1:50051",
grpc.ServerCredentials.createInsecure(),
(error) => {
if (error) throw error;
server.start();
console.log("gRPC server listening on 127.0.0.1:50051");
},
);
Create a client and make a unary call:
const client = new CustomerService(
"127.0.0.1:50051",
grpc.credentials.createInsecure(),
);
const deadline = new Date(Date.now() + 1_000);
client.getCustomer({ id: "1" }, { deadline }, (error, customer) => {
if (error) {
console.error(error.code, error.details);
return;
}
console.log(customer);
});
The example uses an in-memory array and insecure local credentials for clarity. Production services need durable storage, TLS, authentication, authorization, validation, monitoring and graceful shutdown.
REST and gRPC
| Area | REST | gRPC |
|---|---|---|
| API model | Resources and HTTP methods | Services and remote methods |
| Common transport | HTTP/1.1, HTTP/2 or HTTP/3 | HTTP/2 by default |
| Common payload | JSON text | Protobuf binary |
| Contract | Optional OpenAPI or other description | .proto contract is central |
| Client integration | Simple HTTP client | Generated or runtime-loaded stub |
| Streaming | SSE, WebSocket or streamed HTTP responses | Native client, server and bidirectional streams |
| Browser use | Direct and widely supported | Usually requires gRPC-Web |
| Debugging | Human-readable payloads and common tools | Binary payloads need gRPC-aware tools |
| Caching | Natural fit for HTTP resource caching | Usually handled at application or proxy layers |
| Best fit | Public APIs and resource-oriented applications | Internal services and strongly typed low-latency calls |
REST can be the public edge API while gRPC connects internal services:
Browser or third-party client
|
v
REST or GraphQL gateway
|
v
Internal gRPC services
The correct choice depends on clients, operations, infrastructure and team experience. Binary encoding alone does not make a system well designed.
Service Discovery and Load Balancing
A production client usually calls a logical service name rather than a fixed machine.
Client -> service discovery or resolver -> healthy backend addresses
-> load-balancing policy
-> selected gRPC server
Load balancing can happen:
- client side: the gRPC client selects from discovered backends
- proxy side: a load balancer or service mesh selects a backend
The chosen infrastructure must understand long-lived HTTP/2 connections. A connection-level load balancer may keep many RPCs on one backend unless it balances individual streams or clients maintain several connections.
Health checking, readiness signals and graceful draining prevent calls from reaching unhealthy or terminating instances.
Security
Transport Security
Use TLS to encrypt calls and authenticate the server. Internal systems with stronger identity requirements may use mutual TLS so both client and server present certificates.
createInsecure() is suitable only for controlled local examples, not production traffic.
Authentication
Credentials such as tokens can be sent through metadata. Interceptors can validate them and create caller context before the service handler runs.
Authorization
The server must authorize the caller for the specific method and resource. A valid channel or token does not imply permission to access every message.
Input and Resource Limits
- validate every request
- limit message sizes
- bound streaming duration and buffering
- apply quotas and rate limits
- set deadlines
- restrict reflection in sensitive environments when appropriate
- protect secrets in metadata and logs
Observability
Distributed calls need consistent telemetry.
Record:
- service and method name
- gRPC status code
- duration
- request and response size
- retry count
- deadline or cancellation outcome
- peer and deployment information where safe
Propagate trace context through metadata so one request can be followed across services.
Gateway span
`- UserService.GetUser span
`- Database query span
Metrics should separate expected client errors from server failures. Logs should use correlation IDs and avoid recording sensitive message contents.
API Evolution
Safe evolution requires both schema and behavior compatibility.
Prefer:
- adding new fields with safe defaults
- adding new RPC methods instead of changing existing meanings
- preserving deployed field numbers
- reserving removed fields
- keeping old and new clients working during rollout
- versioning packages for intentionally incompatible changes
package customers.v1;
package customers.v2;
Test compatibility in continuous integration and deploy changes in an order that supports mixed client and server versions.
When to Use gRPC
gRPC is a strong fit when:
- internal services need efficient typed communication
- several implementation languages must share one contract
- streaming is a first-class requirement
- low latency and compact messages matter
- code generation improves developer productivity
- the organization can manage Protobuf schema evolution
Consider REST, GraphQL or another approach when:
- browsers or third parties must call the API directly
- human-readable requests are important
- resource caching through standard HTTP infrastructure is central
- the API is small and code generation adds more complexity than value
- clients cannot support HTTP/2 or gRPC-compatible proxies
Common Mistakes
| Mistake | Correct understanding |
|---|---|
| Treating an RPC like a local function call | Networks introduce latency, timeouts, cancellation and partial failure |
| Calling gRPC automatically faster than REST | Performance depends on the complete workload and architecture |
| Reusing a deleted Protobuf field number | Reserve removed names and numbers permanently |
| Omitting deadlines | Every remote call should have a bounded waiting time |
| Retrying every failure | Retry only transient failures and safe or idempotent operations |
| Sharing insecure credentials in production | Use TLS and appropriate authentication |
| Sending unbounded streams | Apply flow control, limits, cancellation and backpressure |
| Assuming Protobuf performs authorization | The service must authenticate and authorize each call |
| Ignoring browser limitations | Use gRPC-Web or expose a browser-friendly gateway |
Returning every failure as INTERNAL | Use the narrowest stable gRPC status code |
| Deploying schema changes without compatibility checks | Support mixed versions and test contract evolution |
| Load balancing only long-lived connections | Ensure the strategy distributes RPC streams effectively |
Summary
gRPC provides strongly typed remote calls using service contracts, Protobuf messages and HTTP/2 transport.
Define services and messages in a .proto contract
|
v
Generate or load client and server interfaces
|
v
Client stub serializes an RPC request
|
v
HTTP/2 carries unary or streaming messages
|
v
Server handler executes business logic
|
v
Response or gRPC status returns to the client
|
v
Deadlines, security, retries and telemetry keep it reliable
A production-ready gRPC system is more than binary serialization. It combines compatible contracts, bounded calls, safe retry behavior, secure channels, healthy load balancing and end-to-end observability.