Skip to main content

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​

ComponentResponsibility
.proto fileDefines services, methods and message types
Protocol BuffersSerialize structured messages into a compact binary format
Code generator or runtime loaderCreates language-specific client and server interfaces
Client stubConverts a method call into a gRPC request
gRPC runtimeManages HTTP/2 streams, metadata, deadlines and message framing
Server stubDecodes the request and invokes the registered handler
Service implementationRuns 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 UserService service
  • the GetUser RPC 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​

TypeExample use
stringNames and identifiers
boolFlags
int32, int64Signed integers
uint32, uint64Unsigned integers
float, doubleDecimal values
bytesRaw binary data
enumA controlled set of named values
repeatedA list of values
nested messageStructured 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:

ApproachBehavior
Generated codeA compiler creates strongly typed source code before the application runs
Runtime loadingThe 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.

StatusTypical meaning
OKOperation completed successfully
INVALID_ARGUMENTInput is malformed regardless of system state
NOT_FOUNDRequested resource does not exist
ALREADY_EXISTSResource creation conflicts with an existing resource
UNAUTHENTICATEDValid authentication credentials are missing
PERMISSION_DENIEDCaller is authenticated but not allowed
FAILED_PRECONDITIONSystem state does not allow the operation
ABORTEDOperation was aborted, often because of concurrency conflict
RESOURCE_EXHAUSTEDQuota or capacity limit was exceeded
DEADLINE_EXCEEDEDDeadline expired before completion
UNAVAILABLEService is temporarily unavailable
INTERNALUnexpected 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​

AreaRESTgRPC
API modelResources and HTTP methodsServices and remote methods
Common transportHTTP/1.1, HTTP/2 or HTTP/3HTTP/2 by default
Common payloadJSON textProtobuf binary
ContractOptional OpenAPI or other description.proto contract is central
Client integrationSimple HTTP clientGenerated or runtime-loaded stub
StreamingSSE, WebSocket or streamed HTTP responsesNative client, server and bidirectional streams
Browser useDirect and widely supportedUsually requires gRPC-Web
DebuggingHuman-readable payloads and common toolsBinary payloads need gRPC-aware tools
CachingNatural fit for HTTP resource cachingUsually handled at application or proxy layers
Best fitPublic APIs and resource-oriented applicationsInternal 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​

MistakeCorrect understanding
Treating an RPC like a local function callNetworks introduce latency, timeouts, cancellation and partial failure
Calling gRPC automatically faster than RESTPerformance depends on the complete workload and architecture
Reusing a deleted Protobuf field numberReserve removed names and numbers permanently
Omitting deadlinesEvery remote call should have a bounded waiting time
Retrying every failureRetry only transient failures and safe or idempotent operations
Sharing insecure credentials in productionUse TLS and appropriate authentication
Sending unbounded streamsApply flow control, limits, cancellation and backpressure
Assuming Protobuf performs authorizationThe service must authenticate and authorize each call
Ignoring browser limitationsUse gRPC-Web or expose a browser-friendly gateway
Returning every failure as INTERNALUse the narrowest stable gRPC status code
Deploying schema changes without compatibility checksSupport mixed versions and test contract evolution
Load balancing only long-lived connectionsEnsure 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.