Metadata
Data about the call, not in the message
Section titled “Data about the call, not in the message”The request and response messages carry your domain data — a user, an order. But some information is about the call itself: who is calling, which trace this belongs to, what request id to log. Putting that inside every message would pollute your schema. gRPC gives it a separate channel: metadata.
Metadata is a set of key/value pairs sent alongside a call, exactly like HTTP headers — which is literally what they become on the HTTP/2 wire.
Headers and trailers
Section titled “Headers and trailers”Metadata comes in two positions:
- Headers travel before the message, at the start of the call. This is where a client puts an auth token or a trace id — anything the server needs up front.
- Trailers travel after the response, at the end of the call. This is where a server puts information known only once work is done — like final status details or server-side metrics. Streaming responses especially rely on trailers, since the status can only be known after the last message.
flowchart LR client["client"] -->|"headers: authorization, x-trace-id"| server["server"] server -->|"response message(s)"| client server -->|"trailers: final status, metrics"| client
Reading and writing metadata
Section titled “Reading and writing metadata”A client sets headers before the call; the server reads them, and can send back headers and trailers.
// Client: attach metadata to the outgoing call.ctx := metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+token, "x-request-id", reqID,)resp, err := client.GetUser(ctx, req)
// Server: read incoming metadata.md, _ := metadata.FromIncomingContext(ctx)reqID := md.Get("x-request-id")# Client: pass metadata as a list of (key, value) tuples.metadata = [("authorization", f"Bearer {token}"), ("x-request-id", req_id)]resp = client.GetUser(request, metadata=metadata)
# Server: read it off the context.def GetUser(self, request, context): incoming = dict(context.invocation_metadata()) req_id = incoming.get("x-request-id")// Client: build a Metadata object.const md = new Metadata();md.set('authorization', `Bearer ${token}`);md.set('x-request-id', reqId);client.getUser(request, md, (err, resp) => { /* ... */ });
// Server: read it off the call.function getUser(call, callback) { const reqId = call.metadata.get('x-request-id')[0];}Rules and conventions
Section titled “Rules and conventions”- Keys are lowercase. gRPC normalizes metadata keys to lowercase ASCII; treat them case-insensitively.
- Binary values use a
-binsuffix. A key ending in-bin(e.g.trace-context-bin) is treated as raw bytes and base64-encoded on the wire; anything else must be a valid ASCII string. - Don’t reuse reserved keys. Keys starting with
grpc-are reserved by the framework — don’t set them yourself. - Metadata is not encrypted by itself — its privacy comes from TLS on the channel. A token in metadata over an insecure channel is exposed.
Propagation across a call chain
Section titled “Propagation across a call chain”Metadata is how context flows through a chain of services. When service A calls B which calls C, you typically propagate the trace id and request id: each hop reads the incoming metadata and copies the relevant keys onto its outgoing calls, usually inside an interceptor so no handler has to remember. That is how a distributed trace stays stitched together across many gRPC hops.