# How Kratos Manages and Propagates Metadata Across Microservices: Context Flow Explained

> Discover how Kratos manages and propagates metadata across microservices using context flow. Learn to bind request-level key-value pairs via HTTP/gRPC headers for efficient interservice communication.

- Repository: [Kratos/kratos](https://github.com/go-kratos/kratos)
- Tags: internals
- Published: 2026-03-02

---

**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)](https://github.com/go-kratos/kratos/blob/main/metadata/metadata.go):

```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)](https://github.com/go-kratos/kratos/blob/main/transport/http/server.go), the filter constructs the transport layer:

```go
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)](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()`:

```go
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)](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)](https://github.com/go-kratos/kratos/blob/main/transport/http/client.go) then constructs the transport:

```go
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)](https://github.com/go-kratos/kratos/blob/main/transport/transport.go)) abstracts header access across protocols:

```go
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)](https://github.com/go-kratos/kratos/blob/main/middleware/tracing/metadata.go), the `Metadata` propagator implements OpenTelemetry's `TextMapPropagator`:

```go
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()`:

```go
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:

```go
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.NewServerContext` and `metadata.NewClientContext` bind metadata to `context.Context`, while `transport.NewServerContext` and `transport.NewClientContext` attach the `Transporter` interface.
- **Transport Abstraction**: The `Transporter` interface bridges metadata to HTTP/gRPC headers via `RequestHeader()` and `ReplyHeader()`, enabling protocol-agnostic middleware.
- **Propagation Flow**: Client-side `AppendToClientContext` injects values into headers; server-side filters extract them back into metadata maps available via `FromServerContext`.

## 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.