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

Implementing a gRPC Server

Learn how to define a gRPC service, generate server stubs, implement unary and streaming handlers, and manage a production server's lifecycle safely.

ImplementationIntermediate10 min readJul 10, 2026
Analogies

Building a gRPC Server

A gRPC server is a process that binds to a TCP port, registers one or more service implementations generated from a .proto file, and dispatches incoming HTTP/2 requests to the correct method handler based on the fully-qualified RPC name in the request path. Unlike a REST controller that matches routes at runtime, a gRPC server's method dispatch table is fixed at code-generation time from the service definition, so every RPC the server can handle is known and type-checked before the process even starts.

🏏

Cricket analogy: Just as a cricket team's fielding positions are set on the team sheet before the toss so every player knows their exact role, a gRPC server's method table is fixed from the .proto file before the process starts, leaving no ambiguity about who handles which delivery.

Defining the Service and Generating Stubs

The server-side workflow starts with a service definition in a .proto file, for example service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); }, compiled with protoc and the relevant language plugin (such as protoc-gen-go-grpc or grpc_tools_node_protoc) to produce base server code containing an abstract interface or class that the developer must implement. In Go this appears as an UnimplementedGreeterServer struct to embed for forward compatibility, while in Python it is an abstract GreeterServicer base class; either way, the generated code handles protobuf serialization, HTTP/2 framing, and method routing so the implementer only writes business logic.

🏏

Cricket analogy: Compiling a .proto file with protoc is like the ICC publishing the official playing conditions before a series so every umpire and team codes their behavior against the same fixed rulebook rather than improvising interpretations.

Implementing Unary and Streaming Methods

A unary handler receives a single request message and returns a single response, making it structurally similar to a normal function call, whereas streaming handlers (server-streaming, client-streaming, or bidirectional) receive or return a stream object that the implementation reads from or writes to in a loop, often within a goroutine or async task to avoid blocking. In server-streaming RPCs such as a stock price feed, the handler calls stream.Send() repeatedly until it has nothing left to send and then returns nil to close the stream, while in bidirectional streaming both sides read and write concurrently over the same HTTP/2 stream, requiring careful handling of context cancellation to avoid goroutine leaks.

🏏

Cricket analogy: A unary RPC is like a single delivery ball where the bowler bowls once and the outcome is immediately known, while a server-streaming RPC is like a bowler sending down an entire over, with results (dot ball, four, wicket) arriving one at a time.

go
type server struct {
    pb.UnimplementedGreeterServer
}

// Unary RPC
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
    if req.GetName() == "" {
        return nil, status.Error(codes.InvalidArgument, "name must not be empty")
    }
    return &pb.HelloReply{Message: "Hello, " + req.GetName()}, nil
}

// Server-streaming RPC
func (s *server) ListGreetings(req *pb.HelloRequest, stream pb.Greeter_ListGreetingsServer) error {
    greetings := []string{"Hi", "Hello", "Hey", "Greetings"}
    for _, g := range greetings {
        if err := stream.Send(&pb.HelloReply{Message: g + ", " + req.GetName()}); err != nil {
            return err
        }
    }
    return nil
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    grpcServer := grpc.NewServer(
        grpc.UnaryInterceptor(loggingInterceptor),
        grpc.MaxRecvMsgSize(4*1024*1024),
    )
    pb.RegisterGreeterServer(grpcServer, &server{})
    reflection.Register(grpcServer)

    go func() {
        sig := make(chan os.Signal, 1)
        signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
        <-sig
        grpcServer.GracefulStop()
    }()

    if err := grpcServer.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

Server Lifecycle, Interceptors, and Graceful Shutdown

Production gRPC servers rarely run bare handlers; instead they wrap grpc.NewServer() with unary and stream interceptors that implement cross-cutting concerns such as authentication, structured logging, panic recovery, and Prometheus metrics, chained together so each request passes through the pipeline before reaching the actual handler. Equally important is lifecycle management: grpcServer.GracefulStop() should be triggered on SIGTERM so the server stops accepting new connections while allowing in-flight RPCs to complete within a bounded timeout, and health-check services (grpc_health_v1) should be registered so orchestrators like Kubernetes can accurately probe readiness before routing traffic.

🏏

Cricket analogy: Interceptors are like the third umpire reviewing every close decision before it's finalized on the scoreboard, and graceful shutdown is like a captain declaring an innings only after ensuring the current over is safely completed.

Use grpc.NewServer() options like grpc.KeepaliveParams and grpc.MaxConcurrentStreams to tune connection behavior for high-throughput services, and always register grpc_health_v1.HealthServer so load balancers and Kubernetes readiness probes can make accurate routing decisions.

Forgetting to call grpcServer.GracefulStop() on shutdown (using os.Exit or a hard kill instead) will abort in-flight RPCs mid-stream, which is especially damaging for client-streaming or bidirectional RPCs where partial writes can corrupt downstream state.

  • A gRPC server's method dispatch table is generated from the .proto file at compile time, not resolved dynamically at runtime like REST routing.
  • protoc plus a language plugin generates an abstract servicer/interface; the developer implements only the business logic inside the generated scaffolding.
  • Unary handlers behave like ordinary functions; streaming handlers read or write a stream object in a loop and must respect context cancellation.
  • Bidirectional streaming requires careful concurrent handling of both send and receive directions on the same HTTP/2 stream.
  • Interceptors provide a single place to implement cross-cutting concerns like auth, logging, and metrics without touching every handler.
  • Graceful shutdown via GracefulStop() combined with the grpc_health_v1 health service ensures zero-downtime deploys under orchestrators like Kubernetes.
  • Setting explicit limits like MaxRecvMsgSize and KeepaliveParams protects the server from resource exhaustion under production load.

Practice what you learned

Was this page helpful?

Topics covered

#ProtocolBuffers#GRPCStudyNotes#WebDevelopment#ImplementingAGRPCServer#Implementing#GRPC#Server#Building#StudyNotes#SkillVeris