gRPC Metadata and Headers

gRPC metadata is a set of key-value pairs that travel alongside an RPC call — separately from the main message payload. Metadata carries information about the call itself: who is calling, in what language, with what token, and for which trace ID. It is the gRPC equivalent of HTTP headers.

Metadata vs Message Payload

gRPC call structure:
┌───────────────────────────────────────────────────────┐
│  HTTP/2 HEADERS frame   ← metadata lives here         │
│  ─────────────────────                                │
│  :method = POST                                       │
│  :path = /user.UserService/GetUser                    │
│  authorization = Bearer eyJhbGciOi...                 │
│  x-request-id  = abc-123-def-456                      │
│  accept-language = en-US                              │
├───────────────────────────────────────────────────────┤
│  HTTP/2 DATA frame      ← your message payload here   │
│  ─────────────────────                                │
│  [protobuf-encoded GetUserRequest bytes]              │
└───────────────────────────────────────────────────────┘

Two Types of Metadata

gRPC distinguishes between metadata sent at the start of a call and metadata sent at the end.

┌──────────────────────┬───────────────────────────────────────────┐
│ Type                 │ When It Travels                           │
├──────────────────────┼───────────────────────────────────────────┤
│ Initial Metadata     │ Before the first message, set by client   │
│                      │ Also sent by server at start of response  │
├──────────────────────┼───────────────────────────────────────────┤
│ Trailing Metadata    │ After the last message, sent by server    │
│                      │ Carries the gRPC status code and message  │
└──────────────────────┴───────────────────────────────────────────┘

Flow diagram:
Client ──[initial metadata]──────────────────────────────► Server
Client ──[request payload]───────────────────────────────► Server
Client ◄──[initial metadata]──────────────────────────── Server
Client ◄──[response payload]─────────────────────────── Server
Client ◄──[trailing metadata + status code]────────────── Server

Metadata Key Naming Rules

Metadata keys follow specific rules that differ from HTTP header naming.

Rules:
  • Keys are case-insensitive: "Authorization" = "authorization"
  • Keys must use lowercase letters, digits, hyphens, and underscores
  • Binary metadata keys MUST end with "-bin"
  • Keys starting with "grpc-" are reserved for gRPC itself

Examples:
  Allowed:   authorization, x-request-id, user-locale, trace-id
  Binary:    custom-data-bin  (value will be base64-encoded)
  Reserved:  grpc-status, grpc-message, grpc-timeout  (do not override)

Sending Metadata from the Client

Go client — attaching metadata to a call:
  import "google.golang.org/grpc/metadata"

  // Create metadata
  md := metadata.New(map[string]string{
    "authorization": "Bearer eyJhbGciOiJIUzI1...",
    "x-request-id":  "abc-123",
    "x-user-locale": "en-US",
  })

  // Attach to context — all calls using this context carry the metadata
  ctx := metadata.NewOutgoingContext(context.Background(), md)

  // Make the call with metadata attached
  resp, err := userClient.GetUser(ctx, &pb.GetUserRequest{UserId: 42})
Python client:
  metadata = [
    ("authorization", "Bearer eyJhbGciOiJIUzI1..."),
    ("x-request-id",  "abc-123"),
  ]

  response = stub.GetUser(request, metadata=metadata)
Java client (blocking stub):
  Metadata headers = new Metadata();
  Metadata.Key<String> authKey =
    Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER);
  headers.put(authKey, "Bearer eyJhbGciOiJIUzI1...");

  UserServiceGrpc.UserServiceBlockingStub stubWithHeaders =
    MetadataUtils.attachHeaders(stub, headers);

  User user = stubWithHeaders.getUser(request);

Reading Metadata on the Server

Go server — reading incoming metadata:
  import "google.golang.org/grpc/metadata"

  func (s *server) GetUser(ctx context.Context,
      req *pb.GetUserRequest) (*pb.User, error) {

    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
      return nil, status.Error(codes.InvalidArgument, "missing metadata")
    }

    // Read a specific key (returns a slice — keys can have multiple values)
    authValues := md.Get("authorization")
    if len(authValues) == 0 {
      return nil, status.Error(codes.Unauthenticated, "missing token")
    }

    token := authValues[0]
    // validate token...
    return &pb.User{Id: req.UserId}, nil
  }

Sending Metadata from the Server

The server can send both initial metadata (before the response message) and trailing metadata (after the response message).

Go server — sending metadata back to client:
  import "google.golang.org/grpc"
  import "google.golang.org/grpc/metadata"

  func (s *server) GetUser(ctx context.Context,
      req *pb.GetUserRequest) (*pb.User, error) {

    // Send initial metadata (before the response)
    initialMD := metadata.Pairs(
      "server-version", "2.1.0",
      "cache-hint",     "ttl=300",
    )
    grpc.SendHeader(ctx, initialMD)

    // Do the work
    user := lookupUser(req.UserId)

    // Send trailing metadata (after the response)
    trailingMD := metadata.Pairs(
      "processing-time-ms", "14",
      "db-query-count",     "2",
    )
    grpc.SetTrailer(ctx, trailingMD)

    return user, nil
  }

Reading Server Metadata on the Client

Go client — reading metadata returned by server:
  var header, trailer metadata.MD

  resp, err := userClient.GetUser(
    ctx,
    &pb.GetUserRequest{UserId: 42},
    grpc.Header(&header),    // capture initial metadata
    grpc.Trailer(&trailer),  // capture trailing metadata
  )

  serverVersion := header.Get("server-version")
  processingTime := trailer.Get("processing-time-ms")

Common Metadata Use Cases

┌────────────────────────┬───────────────────────────────────────────┐
│ Use Case               │ Metadata Key Example                      │
├────────────────────────┼───────────────────────────────────────────┤
│ Authentication         │ authorization: Bearer <token>             │
│ Distributed tracing    │ x-trace-id: abc123, x-span-id: def456     │
│ Rate limiting          │ x-api-key: key_abc123                     │
│ Internationalisation   │ accept-language: fr-FR                    │
│ Request routing        │ x-tenant-id: customer_99                  │
│ Debug information      │ x-debug-mode: true                        │
│ Server response info   │ x-rate-limit-remaining: 490               │
└────────────────────────┴───────────────────────────────────────────┘

Binary Metadata

Some metadata values are not valid UTF-8 text — for example, raw certificate bytes or serialised binary tokens. Keys ending in -bin signal binary values. gRPC automatically base64-encodes them for transport.

Sending binary metadata in Go:
  binaryValue := []byte{0x01, 0x02, 0x03, 0xFF}

  md := metadata.Pairs("custom-token-bin",
    string(binaryValue))   // gRPC base64-encodes this automatically

  ctx := metadata.NewOutgoingContext(ctx, md)

Summary

Metadata travels in HTTP/2 header frames alongside — but separate from — the message payload. Initial metadata travels before the first message. Trailing metadata travels after the last message and always carries the gRPC status code. Common uses include authentication tokens, tracing IDs, and locale settings. Binary metadata uses keys ending in -bin and is base64-encoded automatically.

Leave a Comment

Your email address will not be published. Required fields are marked *