Category: APIs & Communication Tags: grpc protocol-buffers http2

What Is gRPC?

By Oleksandr Andrushchenko — Published on — Modified on
0 Likes
0 Dislikes
What Is gRPC? Protocol Buffers, HTTP/2, Unary RPC, Streaming, and Microservices Architecture
What Is gRPC? Protocol Buffers, HTTP/2, Unary RPC, Streaming, and Microservices Architecture

gRPC is an open-source Remote Procedure Call (RPC) framework that allows applications to invoke methods on remote services as if they were local functions. It is commonly used for communication between microservices, internal APIs, and distributed systems where performance, strongly typed contracts, and support for streaming are important. gRPC typically uses Protocol Buffers (Protobuf) for message serialization and HTTP/2 for transport.

Instead of designing endpoints around URLs and HTTP verbs, developers define services, methods, and request/response messages in a .proto file. gRPC generates client and server code from that contract, reducing manual serialization and API integration work. However, gRPC also introduces operational considerations around deadlines, connection management, load balancing, error handling, and browser compatibility.

Table of Contents

How gRPC Works

gRPC follows a client-server model. A client invokes a method defined by a remote service, and the gRPC runtime handles serialization, transport, response decoding, and communication errors.

For example, an order service may need product information from a catalog service. Instead of constructing an HTTP request manually and parsing JSON, it calls a generated method such as catalog_stub.GetProduct().

gRPC Request Lifecycle

A typical unary gRPC call follows these steps:

  1. The developer defines a service and its request/response messages in a .proto file.
  2. The Protocol Buffers compiler generates language-specific message classes and gRPC service interfaces.
  3. The client creates a request object and invokes a method on a generated stub.
  4. The gRPC runtime serializes the request and sends it over HTTP/2.
  5. The server decodes the message, executes the corresponding method, and constructs a response.
  6. The response is serialized and returned to the client, which receives a typed response object.

The generated client stub hides much of the networking boilerplate. The operation still remains a remote call, however, with latency, timeouts, partial failures, and independent service availability.

gRPC Request Lifecycle
gRPC Request Lifecycle

That distinction matters. A local function call may take microseconds and rarely fail because of network conditions; a remote method invocation can fail after the server has already performed its operation.

Service-Oriented API Design

In gRPC, a service definition contains methods rather than resource URLs. An inventory service might expose GetStock, ReserveStock, and ReleaseReservation.

These operations describe capabilities explicitly. The service contract also specifies the exact request and response message types, making API mismatches easier to detect during development.

gRPC is particularly useful when internal services are maintained by different teams or implemented in different programming languages.

Protocol Buffers and Service Contracts

Protocol Buffers is a language-neutral, schema-based serialization format developed by Google. It defines structured messages using field numbers and types, then encodes them into a compact binary representation.

Although gRPC can use other serialization formats through custom implementations, Protobuf is its standard and most widely used choice.

Defining a .proto File

The following contract defines a product service with one method that retrieves a product by ID.

syntax = "proto3";

package catalog.v1;

service CatalogService {
  rpc GetProduct(GetProductRequest)
      returns (GetProductResponse);
}

message GetProductRequest {
  string product_id = 1;
}

message GetProductResponse {
  string product_id = 1;
  string name = 2;
  int64 price_cents = 3;
  bool available = 4;
}

Each field has a unique number, such as product_id = 1. Protobuf uses field numbers in its wire format rather than transmitting JSON-style property names.

That can reduce payload size, especially for frequently exchanged structured messages.

However, binary serialization is not automatically faster in every workload. End-to-end performance depends on payload size, network latency, application logic, compression, and implementation quality.

Schema Evolution and Compatibility

Protobuf supports backward-compatible schema evolution when changes follow its compatibility rules.

For example, a new optional field can usually be added without breaking older clients:

message GetProductResponse {
  string product_id = 1;
  string name = 2;
  int64 price_cents = 3;
  bool available = 4;

  string category = 5;
}

Older clients can ignore the unknown field, while newer clients can read it.

Field numbers should never be casually reused. If a field is removed, reserve its number and preferably its name to prevent future accidental reuse.

message Product {
  reserved 4;
  reserved "legacy_sku";

  string product_id = 1;
  string name = 2;
  int64 price_cents = 3;
}

Changing field types or semantics can introduce compatibility problems even when the schema still compiles. Schema evolution should be reviewed as carefully as changes to a public REST API.

For related design principles, see API Versioning and Backward Compatibility.

Four Types of gRPC Communication

gRPC supports four communication patterns: unary, server streaming, client streaming, and bidirectional streaming. This is one of its most important differences from conventional request-response REST APIs.

Four Types of gRPC Communication
Four Types of gRPC Communication

All four patterns are defined explicitly in the service contract.

1. Unary RPC

Unary RPC is the simplest pattern: the client sends one request, and the server returns one response.

rpc GetProduct(GetProductRequest)
    returns (GetProductResponse);

This resembles a conventional HTTP API call. It is appropriate for retrieving a record, validating a request, or executing a short operation.

2. Server Streaming RPC

In server streaming, the client sends one request, and the server returns a sequence of responses over the same RPC.

rpc WatchOrder(WatchOrderRequest)
    returns (stream OrderUpdate);

A client can subscribe to order status updates and receive multiple messages as the order moves through processing, shipping, and delivery.

Unlike Server-Sent Events, gRPC streaming uses gRPC message framing and typically requires a gRPC-capable client rather than the browser's native EventSource interface.

3. Client Streaming RPC

In client streaming, the client sends multiple messages and receives one response after completing its stream.

rpc UploadMetrics(stream Metric)
    returns (UploadSummary);

This pattern is useful for sending batches of telemetry, uploading data in chunks, or aggregating multiple client-side measurements into one server response.

Client streaming does not mean the entire upload must be buffered in memory. Implementations can process messages incrementally.

4. Bidirectional Streaming RPC

Bidirectional streaming allows both client and server to send sequences of messages over one RPC. Each side can read and write independently.

rpc Chat(stream ChatMessage)
    returns (stream ChatMessage);

This is useful for interactive systems, real-time coordination, and long-running communication between services.

Message ordering is preserved within each individual stream direction, but gRPC does not define a global ordering between messages sent independently by the two peers.

RPC Type Client Sends Server Sends Example
Unary One message One message Get product details
Server streaming One message Multiple messages Watch job progress
Client streaming Multiple messages One message Upload telemetry
Bidirectional streaming Multiple messages Multiple messages Interactive coordination

gRPC vs. REST and Messaging

gRPC and REST are both useful for service communication, but they optimize for different development and operational needs.

REST is widely supported by browsers, API gateways, monitoring tools, and external consumers. gRPC emphasizes strongly typed contracts, generated clients, and efficient streaming between applications.

Characteristic gRPC REST
API model Remote methods Resources and HTTP methods
Common serialization Protocol Buffers JSON
Transport Typically HTTP/2 HTTP/1.1, HTTP/2, or HTTP/3
Streaming Four native RPC patterns Possible with HTTP streaming techniques
Browser integration Usually requires gRPC-Web or a gateway Native Fetch support
Contract Schema-first by default Can use OpenAPI or other specifications
Typical use Internal service communication Public and internal APIs

When to Choose gRPC

gRPC is a good fit when services communicate frequently, multiple programming languages are involved, and API contracts need strong tooling.

For example, a Python order service, Go inventory service, and Java pricing service can share the same Protobuf definitions and generate compatible clients.

REST is often preferable for public-facing APIs where clients include browsers, third-party developers, or systems without gRPC support.

Messaging is preferable when services should not wait synchronously for one another. A message broker can buffer work and decouple producers from consumers, while a gRPC call normally creates a direct dependency on the remote service's availability.

For a deeper comparison, see REST vs gRPC vs Messaging Between Microservices.

How gRPC Uses HTTP/2

Standard gRPC commonly runs over HTTP/2, which supports multiple concurrent streams over a single connection. This allows several RPCs to share one underlying transport connection.

HTTP/2 also provides binary framing, header compression, and flow control. These capabilities are particularly useful for applications with many concurrent requests or long-lived streams.

HTTP/2 Multiplexing

With HTTP/1.1, clients often open multiple TCP connections to handle concurrent requests efficiently. HTTP/2 allows multiple logical streams to share one connection.

For example, a service can execute several independent unary RPCs while another RPC continuously streams updates.

However, multiplexing does not eliminate every bottleneck. All streams sharing one TCP connection can still be affected by packet loss at the transport layer, and the server may limit the number of concurrent streams.

gRPC Framing and Metadata

gRPC messages are framed inside HTTP/2 data streams. Each message includes a small framing header indicating whether compression is used and the length of the serialized message.

gRPC also uses HTTP/2 headers and trailers to carry metadata and RPC status information.

For example, a successful HTTP/2 response can still contain a non-successful gRPC status. Monitoring systems should inspect the gRPC status rather than treating every HTTP 200 response as a successful RPC.

For more information about transport behavior, see HTTP/1.1 vs HTTP/2 vs HTTP/3.

Deadlines, Retries, and Error Handling

Remote calls fail differently from local function calls. A gRPC client may lose connectivity, exceed its deadline, or receive an application error after the server has already performed part of the requested operation.

Production gRPC clients should use explicit deadlines, distinguish error categories, and retry only operations that can safely tolerate repetition.

Deadlines and Cancellation

A deadline specifies the maximum time allowed for an RPC to complete. Without one, a client may wait much longer than intended during network or service failures.

import grpc

try:
    response = stub.GetProduct(
        GetProductRequest(product_id="p-123"),
        timeout=2.0,
    )

except grpc.RpcError as exc:
    if exc.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
        print("Catalog service timed out")
    else:
        raise

The timeout argument sets the RPC deadline relative to the call.

When a deadline expires, the client stops waiting, but this does not prove the server never executed the operation. For state-changing RPCs, idempotency is essential when the client may retry.

Servers should also check cancellation during long-running operations and avoid continuing expensive work after the caller is no longer interested.

gRPC Status Codes

gRPC defines its own status codes, separate from HTTP response status codes.

Status Meaning Typical Handling
OK Successful RPC Process response
INVALID_ARGUMENT Invalid request Correct client input
NOT_FOUND Resource missing Handle missing entity
UNAUTHENTICATED Missing or invalid identity Authenticate again
PERMISSION_DENIED Operation not authorized Reject or request access
RESOURCE_EXHAUSTED Quota or resource limit exceeded Back off according to policy
UNAVAILABLE Service temporarily unavailable Consider bounded retry
DEADLINE_EXCEEDED RPC deadline expired Handle timeout and uncertain outcome

Retry Safety and Idempotency

Retries are appropriate for some transient failures, but automatically repeating every RPC can create duplicate business operations.

For example, retrying GetProduct is usually safe because it reads data. Retrying ChargeCustomer without an idempotency key could charge the customer twice.

gRPC supports configurable retry behavior in supported clients, but retry policies should be explicit and limited to appropriate status codes and operations.

For state-changing calls, use idempotency identifiers and durable deduplication where necessary.

For a broader discussion, see Timeouts, Retries, and Exponential Backoff.

Production Design Example

Consider an e-commerce platform with separate Order, Catalog, Inventory, and Pricing services. The services are deployed on Kubernetes and communicate internally using gRPC.

The platform receives orders through a public REST API, but internal calls use generated gRPC clients to share strongly typed service contracts.

Service Architecture

The order workflow may look like this:

  1. The API gateway receives an HTTP request to create an order.
  2. The Order service calls Catalog to retrieve product details.
  3. The Order service calls Pricing to calculate the current price.
  4. The Order service calls Inventory to reserve stock.
  5. The Order service persists the order and returns the result.

Each gRPC call has a deadline based on the remaining request budget. If the public API has a three-second response deadline, internal services cannot independently consume three seconds each without exceeding the total budget.

Services should propagate tracing context and relevant authentication metadata, while enforcing authorization at the receiving service.

The architecture must also account for partial failures. For example, Inventory may successfully reserve stock while the Order service fails to persist the order.

gRPC does not solve distributed transaction consistency. Such workflows may require compensating operations or a Saga pattern.

Load Balancing and Connection Management

Long-lived HTTP/2 connections can affect load balancing. If a client opens one connection to a backend and sends hundreds of RPCs over it, connection-level load balancing may keep those requests on the same backend.

This can produce uneven traffic distribution even when the number of TCP connections appears balanced.

Possible solutions include client-side load balancing, a gRPC-aware proxy that balances individual RPC streams, or carefully managed connection pools.

Kubernetes service discovery, Envoy, and other infrastructure components can support these patterns, but the exact behavior depends on the client resolver and proxy configuration.

For related infrastructure concerns, see What Is Service Discovery?.

Observability and Security

A production gRPC deployment should expose metrics for request rate, latency, error codes, active streams, and deadline failures.

Distributed tracing is especially valuable because a single external request may trigger several downstream RPCs.

For internal authentication, mutual TLS (mTLS) can authenticate both sides of a connection. Application-level authorization is still necessary because a trusted service identity does not automatically authorize every operation.

Streaming RPCs also require resource limits. A client that opens many streams or sends messages faster than the server can process them can exhaust memory and connection capacity.

Ready-to-Use Example

The following example builds a working Python gRPC service with unary and server-streaming methods. It uses Protocol Buffers for contracts, generated client/server code, explicit deadlines, and a server that streams progress updates.

The example runs locally without external infrastructure. The same service contract can later be used with Kubernetes, Docker, or another deployment platform.

Dependencies and Setup

Install the Python gRPC runtime and code-generation tools:

python -m pip install grpcio grpcio-tools protobuf

Create a file named catalog.proto:

syntax = "proto3";

package catalog.v1;

service CatalogService {
  rpc GetProduct(GetProductRequest)
      returns (GetProductResponse);

  rpc WatchImport(WatchImportRequest)
      returns (stream ImportProgress);
}

message GetProductRequest {
  string product_id = 1;
}

message GetProductResponse {
  string product_id = 1;
  string name = 2;
  int64 price_cents = 3;
  bool available = 4;
}

message WatchImportRequest {
  string import_id = 1;
}

message ImportProgress {
  string import_id = 1;
  int32 percent = 2;
  string status = 3;
}

Generate the Python message classes and gRPC service interfaces:

python -m grpc_tools.protoc \
    -I. \
    --python_out=. \
    --grpc_python_out=. \
    catalog.proto

The command generates catalog_pb2.py and catalog_pb2_grpc.py. These files contain the message classes, client stub, and server interface.

Python gRPC Server

Save the following as server.py. The server implements a unary product lookup and a server-streaming import progress method.

import asyncio
import grpc

import catalog_pb2
import catalog_pb2_grpc


PRODUCTS = {
    "p-100": {
        "name": "Mechanical Keyboard",
        "price_cents": 8999,
        "available": True,
    },
    "p-200": {
        "name": "Wireless Mouse",
        "price_cents": 3499,
        "available": True,
    },
}


class CatalogService(
    catalog_pb2_grpc.CatalogServiceServicer
):
    async def GetProduct(self, request, context):
        product = PRODUCTS.get(request.product_id)

        if product is None:
            await context.abort(
                grpc.StatusCode.NOT_FOUND,
                "Product not found",
            )

        return catalog_pb2.GetProductResponse(
            product_id=request.product_id,
            name=product["name"],
            price_cents=product["price_cents"],
            available=product["available"],
        )

    async def WatchImport(self, request, context):
        for percent in range(0, 101, 20):
            if context.cancelled():
                return

            await asyncio.sleep(0.5)

            yield catalog_pb2.ImportProgress(
                import_id=request.import_id,
                percent=percent,
                status=(
                    "completed"
                    if percent == 100
                    else "processing"
                ),
            )


async def serve():
    server = grpc.aio.server(
        options=[
            ("grpc.max_receive_message_length", 4 * 1024 * 1024),
            ("grpc.max_send_message_length", 4 * 1024 * 1024),
        ],
    )

    catalog_pb2_grpc.add_CatalogServiceServicer_to_server(
        CatalogService(),
        server,
    )

    server.add_insecure_port("127.0.0.1:50051")

    await server.start()

    print("gRPC server listening on 127.0.0.1:50051")

    try:
        await server.wait_for_termination()
    finally:
        await server.stop(grace=5)


if __name__ == "__main__":
    asyncio.run(serve())

The server uses grpc.aio, the asynchronous Python gRPC API. This allows the server to handle concurrent RPCs without dedicating a thread to every waiting stream.

The product data is stored in memory for demonstration purposes. A real service would query a database or another authoritative data source.

The server binds only to localhost and uses an insecure channel for local testing. Production deployments should use TLS or mTLS as appropriate.

Python gRPC Client

Save the following as client.py. The client creates a channel, invokes the unary method, and consumes progress updates from the server-streaming method.

import asyncio
import grpc

import catalog_pb2
import catalog_pb2_grpc


async def main():
    async with grpc.aio.insecure_channel(
        "127.0.0.1:50051"
    ) as channel:
        stub = catalog_pb2_grpc.CatalogServiceStub(
            channel
        )

        try:
            product = await stub.GetProduct(
                catalog_pb2.GetProductRequest(
                    product_id="p-100"
                ),
                timeout=2.0,
            )

            print(
                product.product_id,
                product.name,
                product.price_cents,
            )

            stream = stub.WatchImport(
                catalog_pb2.WatchImportRequest(
                    import_id="import-42"
                ),
                timeout=10.0,
            )

            async for update in stream:
                print(
                    update.import_id,
                    update.percent,
                    update.status,
                )

        except grpc.aio.AioRpcError as exc:
            print(
                "RPC failed:",
                exc.code(),
                exc.details(),
            )


if __name__ == "__main__":
    asyncio.run(main())

The client uses one channel for both calls. gRPC channels are designed to be reused rather than created for every RPC.

Each operation has an explicit deadline. The streaming call allows more time because it intentionally produces multiple updates over several seconds.

Running and Testing

Start the server in one terminal:

python server.py

Run the client in another terminal:

python client.py

The unary call should return the product details, followed by progress updates from 0% to 100%.

To test error handling, change the product ID to a value that does not exist. The server will return NOT_FOUND, and the client will receive a grpc.aio.AioRpcError.

To test deadline handling, reduce the streaming timeout to one second. The client should receive DEADLINE_EXCEEDED before all progress messages are delivered.

For production deployment, add TLS credentials, authentication interceptors, health checks, tracing, and appropriate load-balancing configuration.

Common Mistakes

gRPC removes much of the boilerplate involved in service communication, but it does not remove the complexity of distributed systems. Several mistakes are particularly common when teams move from REST to gRPC.

Not Setting RPC Deadlines

Calls without explicit deadlines may remain pending longer than the application can tolerate. A slow downstream service can consume resources and increase latency throughout the request chain.

Set deadlines based on the operation's latency budget and propagate remaining time across downstream calls.

Treating Remote Calls Like Local Functions

Generated stubs make remote methods look like ordinary function calls, but every invocation can fail because of network conditions, service restarts, or resource exhaustion.

Design for partial failures and avoid excessive chains of synchronous service dependencies.

Retrying Every Failed RPC

Automatic retries can amplify load during outages and repeat state-changing operations.

Retry only appropriate failures, use bounded backoff, and make operations idempotent where repeated execution is possible.

Ignoring HTTP/2 Load Balancing Behavior

Long-lived HTTP/2 connections can cause uneven traffic distribution when load balancing happens only at the connection level.

Use gRPC-aware routing, client-side balancing, or appropriate connection-management strategies.

Assuming Browsers Support Native gRPC

Browser applications generally cannot use the full native gRPC transport directly through standard browser APIs.

Use gRPC-Web, Connect-compatible tooling, or an HTTP/JSON gateway when browser clients need access to gRPC services.

Production Checklist

Before deploying gRPC between production services, verify that the API contracts, transport configuration, and failure-handling behavior are suitable for the expected workload.

  • Contracts: Version Protobuf schemas carefully and reserve removed field numbers.
  • Deadlines: Set explicit RPC deadlines and propagate remaining request budgets.
  • Retries: Use bounded retry policies and idempotency protection for state-changing operations.
  • Security: Use TLS or mTLS and enforce authorization at the service boundary.
  • Load balancing: Verify behavior with long-lived HTTP/2 connections and concurrent streams.
  • Streaming: Handle cancellation, flow control, and slow consumers.
  • Observability: Track gRPC status codes, latency, active streams, and deadline failures.
  • Compatibility: Test generated clients across supported languages and schema versions.

Frequently Asked Questions

gRPC is widely used in microservice architectures, but its transport model and generated contracts raise questions about compatibility, performance, and deployment.

Is gRPC Faster Than REST?

gRPC can be faster for some workloads because Protobuf messages are compact and HTTP/2 supports efficient multiplexing.

However, performance depends on payload size, network conditions, serialization costs, and application behavior. Benchmark representative workloads before assuming gRPC will outperform REST.

Does gRPC Require HTTP/2?

Standard gRPC commonly uses HTTP/2. Other variants and transports exist, including gRPC-Web, which adapts gRPC communication for browser environments.

Infrastructure components must support the selected gRPC transport correctly, including streaming and trailers where required.

Can Browsers Call gRPC Services Directly?

Browsers generally use gRPC-Web or another browser-compatible protocol rather than native gRPC over HTTP/2.

A proxy or compatible server implementation translates between browser-supported requests and the backend gRPC service.

Is gRPC Good for Microservices?

Yes. gRPC is especially useful for internal microservice communication where strongly typed contracts, generated clients, and efficient request-response or streaming operations are valuable.

It is not always the best choice for public APIs or workflows that benefit more from asynchronous messaging.

Can gRPC Replace a Message Queue?

Not generally. gRPC provides direct communication between services, while message queues can buffer work, decouple service availability, and support asynchronous processing.

Many architectures use both: gRPC for synchronous operations and message brokers for background tasks and event-driven workflows.

Conclusion

gRPC provides a strongly typed, efficient framework for communication between services. Its combination of Protocol Buffers, generated clients, HTTP/2, and native streaming makes it particularly useful for internal APIs and distributed applications.

However, gRPC is still remote communication. Deadlines, retries, authentication, load balancing, partial failures, and schema evolution remain essential production concerns.

Key Takeaway: gRPC is a strong choice for high-performance, contract-driven service communication, especially between microservices. Use it when typed APIs and streaming provide clear benefits, but combine it with sound distributed-systems practices rather than treating remote calls as ordinary local functions.

These related articles cover API architecture, service communication, and production reliability.

Author

Enjoyed this article?

Support Oleksandr Andrushchenko

Buy me a coffee

This helps Oleksandr Andrushchenko continue creating useful content

Comments (0)