The most surprising thing about gRPC server reflection is that it’s an opt-in feature, and the default gRPC experience for many developers doesn’t include it, despite its immense utility.

Let’s see it in action. Imagine you have a simple gRPC service defined in a .proto file:

syntax = "proto3";

package mypackage;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

Normally, to call SayHello, you’d need the generated client code for this specific service. But with reflection, you can use a generic gRPC client that discovers the service at runtime.

Here’s a Python example using grpc_tools.protoc to generate code for a server that enables reflection, and then a generic client that uses it:

First, save the proto as greeter.proto. Then, generate the necessary Python files:

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

Now, for the server. We need to add the reflection package and hook it up.

import grpc
from concurrent import futures
import time

# Import generated code
import greeter_pb2
import greeter_pb2_grpc

# Import reflection
from grpc_reflection.v1alpha import reflection_pb2
from grpc_reflection.v1alpha import reflection_pb2_grpc

class GreeterServicer(greeter_pb2_grpc.GreeterServicer):
    def SayHello(self, request, context):
        return greeter_pb2.HelloReply(message=f"Hello, {request.name}!")

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    greeter_pb2_grpc.add_GreeterServicer_to_server(GreeterServicer(), server)

    # --- Reflection Setup ---
    SERVICE_NAMES = (
        reflection_pb2.FILE_DESCRIPTOR_SET_FIELD_NUMBER,
        'mypackage.Greeter',
    )
    reflection_info = reflection_pb2_grpc.reflection_info()
    reflection_pb2_grpc.add_reflection_to_server(server, reflection_info, SERVICE_NAMES)
    # --- End Reflection Setup ---

    server.add_insecure_port('[::]:50051')
    server.start()
    print("Server started on port 50051 with reflection enabled.")
    try:
        while True:
            time.sleep(86400)
    except KeyboardInterrupt:
        server.stop(0)

if __name__ == '__main__':
    serve()

The key part is reflection_pb2_grpc.add_reflection_to_server(server, reflection_info, SERVICE_NAMES). This registers a special service (grpc.reflection.v1alpha.ServerReflection) that clients can query. SERVICE_NAMES tells the reflection service which services and file descriptors it should advertise.

Now, for the generic client. We don’t need the greeter_pb2_grpc module here for the discovery part.

import grpc
from grpc_reflection.v1alpha import reflection_pb2
from grpc_reflection.v1alpha import reflection_pb2_grpc

def run_reflection_client():
    channel = grpc.insecure_channel('localhost:50051')
    reflection_stub = reflection_pb2_grpc.ServerReflectionStub(channel)

    # Request list of all services
    request = reflection_pb2.ServerReflectionRequest(list_services='')
    responses = reflection_stub.ServerReflectionInfo(iter([request]))

    print("Discovered Services:")
    for response in responses:
        if response.list_services_response:
            for service in response.list_services_response.service:
                print(f"- {service.name}")

    # Example: Get schema for a specific service
    request = reflection_pb2.ServerReflectionRequest(file_by_filename='greeter.proto')
    responses = reflection_stub.ServerReflectionInfo(iter([request]))

    for response in responses:
        if response.file_descriptor_response:
            print("\nFile Descriptor for greeter.proto:")
            print(response.file_descriptor_response.file_descriptor_proto[0].decode('utf-8'))

    channel.close()

if __name__ == '__main__':
    run_reflection_client()

This client doesn’t know about Greeter or HelloRequest beforehand. It asks the server, "What services do you have?" and then, "Give me the .proto definition for greeter.proto." The server, thanks to the reflection hook, responds with this information.

The problem server reflection solves is the tight coupling between client and server generated code. Without it, if you change your .proto file, you must regenerate and redeploy all clients. With reflection, a client can adapt. Tools like grpcurl or API gateways can use reflection to understand and interact with gRPC services without prior knowledge.

Internally, the ServerReflection service defines a few key RPCs:

  • ServerReflectionInfo: A bidirectional streaming RPC. Clients send ServerReflectionRequest messages, and the server responds with ServerReflectionResponse messages.
  • list_services: A request type to get a list of all fully qualified service names.
  • file_by_filename: A request type to get the FileDescriptorProto for a given filename.
  • file_containing_symbol: A request type to get the FileDescriptorProto that contains a specific symbol (like a service or message).
  • file_containing_extension: Similar, but for extension identifiers.

The server needs to have access to the compiled FileDescriptor objects for all its services. The grpc_reflection library handles serializing these into FileDescriptorProto messages. The SERVICE_NAMES tuple passed to add_reflection_to_server is crucial; it acts as a manifest of what the reflection service should expose.

A detail often missed is that reflection doesn’t magically provide runtime type checking for the content of messages. It provides the schema (the structure of messages and services). You still need to validate that the data you send conforms to that schema, which your generated client code or a custom client would do. Reflection is about discovery and understanding the API surface.

The next concept you’ll likely encounter is how to secure gRPC services, especially when using features like reflection that expose your API details.

Want structured learning?

Take the full Grpc course →