How Kratos Manages and Propagates Metadata Across Microservices: Context Flow Explained
Kratos treats metadata as the canonical mechanism for carrying request-level key-value pairs between services, binding a map[string][]string to context.Context via transport-specific helpers that bridge HTTP/gRPC headers.
The go-kratos/kratos framework provides a unified abstraction for managing and propagating metadata across distributed microservices. By decoupling the storage format from the transport layer, Kratos enables seamless context sharing—such as tracing IDs, user identities, and custom headers—across HTTP and gRPC calls without exposing wire-format details to business logic.
Metadata Representation
At the core of the system lies the Metadata type defined in [metadata/metadata.go](https://github.com/go-kratos/kratos/blob/main/metadata/metadata.go):
type Metadata map[string][]string
This structure stores key-value pairs where each key maps to a slice of strings, supporting multi-value headers. Keys are normalized to lowercase on insertion, ensuring case-insensitive lookup regardless of how upstream services format their headers.
The package provides utility methods—including Add(), Set(), Get(), Values(), and Clone()—to manipulate metadata safely. For example, metadata.New() creates an empty map ready for population.
Server-Side Context Attachment
When an HTTP or gRPC request arrives, the server filter creates a Transporter that holds the request headers, then binds both the transporter and metadata to a server-side context.
In [transport/http/server.go](https://github.com/go-kratos/kratos/blob/main/transport/http/server.go), the filter constructs the transport layer:
tr := &Transport{
operation: pathTemplate,
reqHeader: headerCarrier(req.Header),
replyHeader: headerCarrier(w.Header()),
}
tr.request = req.WithContext(transport.NewServerContext(ctx, tr))
The transport.NewServerContext function (defined in [transport/transport.go](https://github.com/go-kratos/kratos/blob/main/transport/transport.go)) stores the Transporter inside the context. Separately, metadata.NewServerContext attaches the parsed request metadata to the same context, making values available to downstream handlers via metadata.FromServerContext(ctx).
Client-Side Context Propagation
Before sending a request, the client injects metadata into the outbound context using helpers like metadata.AppendToClientContext():
ctx := metadata.AppendToClientContext(context.Background(),
"x-request-id", "12345",
"user-id", "alice")
This function (located at [metadata/metadata.go](https://github.com/go-kratos/kratos/blob/main/metadata/metadata.go) line 104) merges user-defined key-value pairs into the context's metadata map.
The HTTP client wrapper in [transport/http/client.go](https://github.com/go-kratos/kratos/blob/main/transport/http/client.go) then constructs the transport:
ctx = transport.NewClientContext(ctx, &Transport{
endpoint: client.opts.endpoint,
reqHeader: headerCarrier(req.Header),
operation: c.operation,
})
transport.NewClientContext stores the Transporter containing the header carrier, ensuring metadata flows into the actual HTTP headers during the round-trip.
The Transport Bridge
The Transporter interface (defined in [transport/transport.go](https://github.com/go-kratos/kratos/blob/main/transport/transport.go)) abstracts header access across protocols:
type Transporter interface {
Kind() Kind
Endpoint() string
Operation() string
RequestHeader() Header
ReplyHeader() Header
}
Server-side, the HTTP filter copies inbound request headers into tr.reqHeader. After the handler executes, it fills tr.replyHeader with response headers.
Client-side, the wrapper stores request headers in reqHeader. After the HTTP call completes, it writes response headers back to tr.replyHeader (ht.replyHeader = headerCarrier(resp.Header)).
Because the same Transporter lives inside the context, middlewares can read or write metadata without knowing whether the underlying transport is HTTP or gRPC.
Middleware Integration: Tracing Propagation
The tracing middleware demonstrates how metadata propagates across service boundaries. In [middleware/tracing/metadata.go](https://github.com/go-kratos/kratos/blob/main/middleware/tracing/metadata.go), the Metadata propagator implements OpenTelemetry's TextMapPropagator:
func (b Metadata) Inject(ctx context.Context, carrier propagation.TextMapCarrier) {
if app, ok := kratos.FromContext(ctx); ok {
carrier.Set(serviceHeader, app.Name())
}
}
func (b Metadata) Extract(parent context.Context, carrier propagation.TextMapCarrier) context.Context {
name := carrier.Get(serviceHeader)
if name == "" { return parent }
md, ok := metadata.FromServerContext(parent)
if !ok { md = metadata.New() }
md.Set(serviceHeader, name)
return metadata.NewServerContext(parent, md)
}
When outgoing requests leave the client, Inject adds the service name to headers. Upon arrival, the server's Extract method pulls values from the carrier back into metadata.Metadata, making them available to any downstream handler or middleware.
Complete Implementation Examples
Reading Metadata in a Service Handler
Access propagated values in your business logic using metadata.FromServerContext():
func (s *myService) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
md, _ := metadata.FromServerContext(ctx)
requestID := md.Get("x-request-id") // Returns "12345"
log.Infof("handling request %s", requestID)
// Add response metadata
md.Set("x-response-id", "resp-6789")
return &pb.HelloReply{Message: "hi " + req.Name}, nil
}
Attaching Metadata from the Client
Inject custom headers before invoking a remote service:
func callGreeter(c pb.GreeterClient) {
ctx := metadata.AppendToClientContext(context.Background(),
"x-request-id", "abc-123",
"user-id", "bob")
resp, err := c.SayHello(ctx, &pb.HelloRequest{Name: "Bob"})
if err != nil { log.Error(err); return }
// Read response metadata
if md, ok := metadata.FromClientContext(ctx); ok {
fmt.Println("Response ID:", md.Get("x-response-id"))
}
}
Summary
- Storage: Kratos uses
metadata.Metadata(map[string][]string) with lowercase key normalization to store request-level data. - Context Binding:
metadata.NewServerContextandmetadata.NewClientContextbind metadata tocontext.Context, whiletransport.NewServerContextandtransport.NewClientContextattach theTransporterinterface. - Transport Abstraction: The
Transporterinterface bridges metadata to HTTP/gRPC headers viaRequestHeader()andReplyHeader(), enabling protocol-agnostic middleware. - Propagation Flow: Client-side
AppendToClientContextinjects values into headers; server-side filters extract them back into metadata maps available viaFromServerContext.
Frequently Asked Questions
How does Kratos handle case sensitivity in metadata keys?
Kratos normalizes all metadata keys to lowercase when inserting into the Metadata map. This ensures that headers like X-Request-ID and x-request-id resolve to the same key, preventing case-related lookup failures across different microservices.
What is the difference between server context and client context in Kratos?
metadata.NewServerContext creates a context for incoming requests, extracting metadata from transport headers and making it available to handlers. metadata.NewClientContext (or AppendToClientContext) prepares the context for outgoing requests, encoding metadata into headers before the network call. Server context reads inbound data; client context writes outbound data.
How do I access propagated metadata inside a middleware?
Use metadata.FromServerContext(ctx) to retrieve the Metadata map from an incoming request context. You can then read values with md.Get("key") or add new values with md.Set(). Because the metadata lives in the standard context.Context, it remains accessible throughout the entire request lifecycle, including in middleware chains.
Can metadata propagate bidirectionally between services?
Yes. Request metadata flows from client to server via request headers, while response metadata flows back via response headers. On the client side, after the RPC completes, you can access response metadata using metadata.FromClientContext(ctx), which reads values populated from the HTTP response headers by the transport layer.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →