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.
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
1. In a generated Go gRPC server, what is the purpose of embedding `pb.UnimplementedGreeterServer` in the server struct?
2. What is the main structural difference between a unary handler and a server-streaming handler?
3. Why should a gRPC server call GracefulStop() instead of terminating the process immediately on SIGTERM?
4. What is the primary role of a unary interceptor in a gRPC server?
5. Why is registering the grpc_health_v1 health service important in a Kubernetes deployment?
Was this page helpful?
You May Also Like
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.
gRPC Error Handling
Understand gRPC's canonical status code model, how to choose the right code and attach structured details, and how to propagate and recover from errors safely.
gRPC Deadlines and Cancellation
Understand how gRPC deadlines bound RPC execution time, how they propagate across service chains, and how explicit cancellation stops unneeded work.
gRPC Metadata and Headers
Learn how gRPC carries cross-cutting information like auth tokens and trace IDs via initial and trailing metadata, separate from the protobuf message body.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics
Web DevelopmentNext.js Study Notes
JavaScript · 30 topics