100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Protocol Buffers

Implementing a gRPC Client

Learn how to establish a gRPC channel, call unary and streaming methods with a generated stub, and configure retries and load balancing for production use.

ImplementationIntermediate9 min readJul 10, 2026
Analogies

Building a gRPC Client

A gRPC client is built by first establishing a grpc.ClientConn (or its language equivalent) to a target address, which manages an underlying HTTP/2 connection including connection pooling, automatic reconnection with exponential backoff, and load balancing across multiple backend addresses when a resolver returns more than one. From that connection, the generated code produces a strongly-typed stub, such as NewGreeterClient(conn), whose methods mirror the service definition exactly, so calling client.SayHello(ctx, req) is type-checked at compile time and automatically handles protobuf marshaling, HTTP/2 framing, and unmarshaling of the response.

🏏

Cricket analogy: Establishing a ClientConn is like a broadcaster setting up the satellite uplink to the stadium before commentary starts, a one-time setup that then lets every ball-by-ball feed (RPC call) flow without renegotiating the connection each time.

Making Unary and Streaming Calls

Calling a unary method on the stub blocks (or awaits, in async languages) until the server responds or the context deadline is exceeded, returning either the response message or an error wrapping a gRPC status code that the caller should check with status.FromError(err) rather than string comparison. For streaming calls, the client obtains a stream object from the stub, for instance stream, err := client.ListGreetings(ctx, req), and then repeatedly calls stream.Recv() in a loop until it receives an io.EOF sentinel signaling the server has finished sending, and for client- or bidirectional-streaming RPCs the client calls stream.Send() for each outbound message and stream.CloseSend() when done writing.

🏏

Cricket analogy: A unary call is like requesting a single DRS review and waiting for the third umpire's one verdict, while a server-streaming call is like watching the live ball-by-ball scorecard update continuously until the innings ends (io.EOF equivalent).

python
import grpc
import greeter_pb2
import greeter_pb2_grpc

def run():
    # Channel setup: connection pooling and retry handled internally
    options = [("grpc.keepalive_time_ms", 30000)]
    with grpc.insecure_channel("localhost:50051", options=options) as channel:
        stub = greeter_pb2_grpc.GreeterStub(channel)

        # Unary call with a 2-second deadline
        try:
            response = stub.SayHello(
                greeter_pb2.HelloRequest(name="Priya"), timeout=2.0
            )
            print("Unary response:", response.message)
        except grpc.RpcError as e:
            print(f"Unary call failed: {e.code()} - {e.details()}")

        # Server-streaming call
        request = greeter_pb2.HelloRequest(name="Priya")
        try:
            for reply in stub.ListGreetings(request, timeout=5.0):
                print("Streamed:", reply.message)
        except grpc.RpcError as e:
            print(f"Stream failed: {e.code()} - {e.details()}")

if __name__ == "__main__":
    run()

Connection Management, Retries, and Load Balancing

A single ClientConn is designed to be created once and reused across the application's lifetime, since it maintains a persistent HTTP/2 connection (or several, under client-side load balancing) and internally multiplexes many concurrent RPCs over that connection rather than opening a new TCP handshake per call; creating a new connection per request is a common performance mistake. Production clients should configure a service config with retry policy (e.g. maxAttempts, initialBackoff, retryableStatusCodes) so transient failures like UNAVAILABLE are retried automatically, and should specify a resolver and load-balancing policy such as round_robin when the target resolves to multiple backend addresses, since gRPC does client-side load balancing rather than relying solely on an external proxy.

🏏

Cricket analogy: Reusing a single ClientConn is like a team keeping the same warmed-up bowler operating through several overs instead of bringing on a fresh, unwarmed bowler for every single delivery, which would be wasteful and slow.

Always reuse a single ClientConn per target across the lifetime of your application (or pool a small number for very high-throughput services); gRPC-Go and grpc-python already multiplex concurrent RPCs efficiently over one HTTP/2 connection.

Blindly retrying every RPC on failure is unsafe for non-idempotent unary calls (e.g. a payment charge); only mark retryable status codes like UNAVAILABLE in the service config, and use idempotency keys for mutating operations.

  • A ClientConn should be created once and reused; it manages persistent HTTP/2 connections, pooling, and automatic reconnection internally.
  • Generated client stubs mirror the .proto service definition exactly, giving compile-time type safety for every RPC call.
  • Unary calls block or await until a response or deadline; streaming calls use Recv()/Send() loops with io.EOF or CloseSend() to signal completion.
  • Errors should be inspected via status.FromError(err) to get a structured gRPC status code, not by parsing error message strings.
  • Service config can define automatic retry policies for transient failures like UNAVAILABLE, but only for idempotent operations.
  • gRPC performs client-side load balancing (e.g. round_robin) across resolved backend addresses rather than relying purely on an external proxy.
  • Deadlines should be set on every call to avoid unbounded blocking; a missing deadline can hang a client indefinitely on a stalled server.

Practice what you learned

Was this page helpful?

Topics covered

#ProtocolBuffers#GRPCStudyNotes#WebDevelopment#ImplementingAGRPCClient#Implementing#GRPC#Client#Building#StudyNotes#SkillVeris