gRPC Metadata — Sending Custom Headers for Auth, Tracing, and Routing
In this tutorial, you will learn about grpc metadata. We cover key concepts, practical examples, and best practices to help you master this topic.
gRPC metadata provides key-value pairs sent alongside RPC calls as headers or trailers, enabling authentication tokens, correlation IDs, tracing context, and custom routing information to flow between clients and servers.
What You'll Learn
- What gRPC metadata is and how it differs from HTTP headers
- Sending metadata from clients
- Reading metadata on servers
- Setting response headers and trailers
- Metadata conventions for auth, tracing, and routing
Why It Matters
Metadata carries cross-cutting concerns like auth tokens, tracing spans, and correlation IDs without polluting the protobuf message definitions. This keeps your service contracts clean while supporting infrastructure requirements. DodaTech's Durga Antivirus Pro passes JWT tokens, correlation IDs, and region hints through gRPC metadata across 50+ Microservices.
Real-World Use
A request arrives at the API Gateway with a JWT token in the Authorization header. The gateway extracts the token and passes it as gRPC metadata. The downstream service reads the metadata, validates the token, and uses the user ID from it — all without adding auth fields to the protobuf definition.
sequenceDiagram
participant Client
participant Server
Client->>Server: Unary Call
Note over Client,Server: Metadata (headers):
authorization: bearer xyz
correlation-id: abc-123
x-region: us-east
Server->>Server: Extract metadata
Server->>Server: Validate token
Server->>Server: Process request
Server-->>Client: Response
Note over Server,Client: Trailers:
x-request-id: req-456
x-process-time: 42ms
Code Examples
Example 1: Sending Metadata from Go Client
package main
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
func sendRequest() {
conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
defer conn.Close()
client := pb.NewThreatServiceClient(conn)
// Create metadata
md := metadata.Pairs(
"authorization", "Bearer eyJhbGci...",
"correlation-id", "abc-123-def-456",
"x-region", "us-east-1",
)
// Send with outgoing context
ctx := metadata.NewOutgoingContext(context.Background(), md)
response, err := client.ReportThreat(ctx, &pb.ThreatRequest{
Name: "Ransomware-X",
Severity: pb.Severity_CRITICAL,
})
}
Example 2: Reading Metadata in Python Server
import grpc
from grpc_interceptor import ServerInterceptor
class MetadataInterceptor(ServerInterceptor):
def intercept(self, method, request, context, method_name):
# Read incoming metadata
auth = dict(context.invocation_metadata())
token = auth.get("authorization", "")
correlation_id = auth.get("correlation-id", "")
region = auth.get("x-region", "default")
print(f"Correlation ID: {correlation_id}")
print(f"Region: {region}")
# Validate auth
if not token.startswith("Bearer "):
context.abort(
grpc.StatusCode.UNAUTHENTICATED,
"Missing or invalid token",
)
# Set response headers
context.send_initial_metadata([
("x-request-id", generate_request_id()),
])
return method(request, context)
# Server
server = grpc.server(
grpc.insecure_server(),
interceptors=[MetadataInterceptor()],
)
Example 3: Propagation Across Services
const grpc = require('@grpc/grpc-js');
const { Metadata } = grpc;
class MetadataPropagator {
constructor() {
this._metadata = new Metadata();
}
// Called by interceptor on incoming request
captureMetadata(call) {
const incoming = call.metadata.getMap();
this._metadata.set('authorization', incoming['authorization'] || '');
this._metadata.set('correlation-id', incoming['correlation-id'] || uuid());
this._metadata.set('x-service', incoming['x-service'] || 'unknown');
}
// Apply to outgoing calls to downstream services
attachMetadata() {
const md = new Metadata();
for (const [key, values] of this._metadata.getMap()) {
md.set(key, values);
}
md.set('x-hop', this._metadata.get('x-hop') || '1');
return md;
}
}
// Usage in a service that calls downstream
async function handleRequest(call, callback) {
const propagator = new MetadataPropagator();
propagator.captureMetadata(call);
// Call downstream service with propagated metadata
const downstreamClient = new DownstreamClient(
'localhost:50052',
grpc.credentials.createInsecure(),
);
downstreamClient.ProcessData(
request,
propagator.attachMetadata(),
(err, response) => {
if (err) callback(err);
else callback(null, response);
},
);
}
Common Mistakes
- Putting large data in metadata — metadata is limited to 8KB total. Store large data in protobuf messages, not metadata.
- Forgetting to propagate metadata — when service A calls service B, metadata from the original request (auth, correlation ID) must be forwarded manually.
- Using mutable metadata across concurrent calls — metadata objects are not thread-safe. Create new metadata for each outgoing call instead of reusing.
- Not handling binary metadata — binary metadata keys must end with
-binand values must be base64-encoded. - Ignoring metadata size limits — gRPC limits metadata to 8KB by default. Configure max metadata size for large headers.
Practice Questions
- What is the difference between gRPC metadata and protobuf message fields?
- How do you read incoming metadata in a gRPC server interceptor?
- Why should you propagate metadata across service boundaries?
- What is the 8KB metadata limit and what happens if you exceed it?
- How do you send binary data in metadata?
Challenge: Build a metadata propagation system that automatically forwards authorization, correlation ID, and tracing headers across a chain of 3 gRPC services, adding a hop counter and service name at each step.
Mini Project
Create a gRPC interceptor library that handles metadata propagation across a service mesh: capture incoming metadata, validate auth tokens, add tracing spans, forward correlation IDs to downstream calls, and add response trailers with request timing and service info.
FAQ
What's Next
Learn about gRPC interceptors for middleware
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro