The missing menu for your gRPC service
You are debugging a gRPC service in a staging environment. You have the IP address and the port, but the proto file is buried in a monorepo you don't have access to, or the version on disk doesn't match what's running. You try to call a method and get UNIMPLEMENTED. You guess the request shape and get INVALID_ARGUMENT. Without reflection, gRPC is a black box. You cannot see what methods exist or what they expect.
Reflection fixes this. It adds a standard service to your server that answers questions about the API. Clients can ask "What methods do you have?" and "What does the ListUsers request look like?" The server responds with the full schema. You get a self-describing API without leaving the gRPC protocol. Tools like grpcurl and grpcui rely on this to provide CLI and web interfaces for any gRPC service.
What reflection actually does
gRPC is designed for efficiency. It strips metadata and descriptions from the wire format to save bandwidth. The binary protocol carries only the data you send. This is great for performance but terrible for introspection. Reflection adds back the metadata, but only when explicitly requested.
The google.golang.org/grpc/reflection package provides a pre-built implementation of the ServerReflection service. When you register it on your server, the library attaches a handler that listens for ServerReflectionInfo requests. This is a server-streaming RPC. The client sends a query, and the server streams back the protobuf descriptors.
These descriptors are embedded in your generated .pb.go files during compilation. The reflection service reads them at runtime. It maps the internal registry of services to the reflection protocol. You do not write the reflection logic. The library handles the translation between your generated code and the introspection requests.
Reflection turns your binary into a self-documenting service.
Minimal setup
Here is the simplest way to enable reflection. You import the package, register it on the server, and start serving. The registration adds the reflection service to the server's method table.
package main
import (
"log"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
// main starts a gRPC server with reflection enabled.
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
// reflection.Register attaches the ServerReflection service to the server.
// This allows clients to query the server for service definitions at runtime.
reflection.Register(s)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
If you forget to import the reflection package, the compiler rejects the build with undefined: reflection. If you register services but forget to call reflection.Register, the server starts normally, but introspection tools return an empty list of services. The reflection service is not enabled by default. You must opt in.
How the runtime handles introspection
When reflection.Register(s) executes, the function calls the internal registration logic. It adds a handler for the method path grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo. This handler is just another RPC method in the server's eyes. It shares the same lifecycle and connection handling as your business logic.
When a client sends a reflection request, the handler scans the server's service registry. It finds every service registered via pb.Register...Server. For each service, it extracts the protobuf descriptors. These descriptors contain the service name, method names, input message types, output message types, and field definitions. The handler streams these descriptors back to the client.
The client reconstructs the schema from the descriptors. Tools like grpcurl use this to display a list of services and methods. grpcui uses it to render a web form where you can fill in request fields and call methods. The reflection service does not execute your business logic. It only returns metadata.
Reflection is plumbing. Run it through every long-lived call site.
Realistic server with grpcurl verification
Here is a realistic server with a service implementation. The code shows how to register both your service and reflection. The server struct embeds the unimplemented interface, which is the standard Go convention for forward compatibility.
// server implements the pb.HelloServiceServer interface.
// Embedding the unimplemented struct ensures new methods return UNIMPLEMENTED automatically.
type server struct {
pb.UnimplementedHelloServiceServer
}
// SayHello returns a greeting message.
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
// Return the response with the name from the request.
return &pb.HelloResponse{Message: "Hello " + in.Name}, nil
}
Embedding UnimplementedHelloServiceServer is a safety net. If the proto file adds a new method later, your server will return UNIMPLEMENTED for that method instead of panicking or silently failing. The compiler does not check for missing methods in embedded structs, so this is a runtime guarantee. The receiver name s follows the convention of using one or two letters matching the type.
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("listen failed: %v", err)
}
s := grpc.NewServer()
// Register the application service.
pb.RegisterHelloServiceServer(s, &server{})
// Register reflection to enable introspection tools.
reflection.Register(s)
log.Println("server listening on :50051")
if err := s.Serve(lis); err != nil {
log.Fatalf("serve failed: %v", err)
}
}
You must register reflection before calling s.Serve. The server does not reload its method table after starting. If you register reflection after the server is running, the service is not added.
Here is how you verify reflection works using grpcurl. This command lists all services registered on the server.
grpcurl -plaintext localhost:50051 list
# output:
hello.HelloService
grpc.reflection.v1alpha.ServerReflection
The output shows your HelloService and the ServerReflection service. The presence of ServerReflection proves that introspection is active. You can now query methods and messages without the proto file.
Reflection is a gift to your future self debugging at 2 AM.
Pitfalls and production considerations
Reflection is a powerful development tool, but it has trade-offs. The most common pitfall is order of operations. You must call reflection.Register before s.Serve. If you register after serving, the reflection service is missing. The server starts fine, but tools fail to find any services.
Another pitfall is multiple servers. If your application creates multiple grpc.Server instances, you must register reflection on each one. The registration is scoped to the server instance. It does not propagate to other servers.
Reflection exposes the full API surface. An attacker can see every method name and message definition. This does not bypass authentication or authorization. The attacker still needs credentials to call a method. But reflection helps map the attack surface. If you have internal methods like AdminResetPassword, reflection reveals them. Some teams disable reflection in production to reduce information leakage. You can register reflection conditionally based on a build tag or environment variable.
Reflection also consumes resources. It is a service that handles requests. The overhead is minimal, but it is there. The reflection handler scans the registry and streams descriptors. For high-throughput servers, this is negligible. For constrained environments, every byte counts.
The worst goroutine bug is the one that never logs.
When to use reflection
Use reflection when you are building a service that developers will interact with using CLI tools like grpcurl or grpcui.
Use reflection when you want to generate client code dynamically without distributing proto files.
Use reflection when you are debugging a running service and need to inspect the schema.
Skip reflection when you are deploying to a highly restricted environment where every byte of attack surface matters.
Skip reflection when you control the client and can bundle the proto files statically.
Register reflection before you serve. The server doesn't reload.