17 Adding Metadata to a gRPC Request
gRPC has become the de facto standard for service-to-service communication in many companies that adopt microservices. Its efficiency makes it a great fit for large-scale systems. In practice, however, the need for more “contextual” communication often arises—for example, sending an authentication token, a trace id, or a custom header with every request. This is where metadata in gRPC becomes important: it provides an extra channel for sending out-of-band data without modifying the core protocol definition.
In this article, I’ll cover gRPC request metadata thoroughly—from the basic theory and an architectural walkthrough to code examples in Node.js and Go. Beyond the examples, I’ll also share best practices and the pitfalls you need to watch out for. Let’s get started!
What Is Metadata in gRPC?
Much like HTTP headers, metadata in gRPC is a set of key-value pairs sent as part of every RPC request or response. They are used for additional information such as:
- Authentication tokens
- Trace/Correlation IDs for observability
- Content options
- Tenant info (multi-tenant apps)
- And any other external information you don’t want to ‘pollute’ the main payload with
Diagram of the Metadata Flow in gRPC
Let’s visualize the flow with the following mermaid diagram:
sequenceDiagram
participant Client
participant Network
participant Server
Client->>Network: gRPC Request (Metadata + Payload)
Network->>Server: gRPC Request (Metadata + Payload)
Server-->>Network: gRPC Response (Metadata + Payload)
Network-->>Client: gRPC Response (Metadata + Payload)
As you can see, both the request and the response can carry metadata.
A Real-World Use Case: The “User Authentication” Scenario
One of the most common scenarios is passing a JWT (JSON Web Token) on every request as proof of authentication. In gRPC, this is not done through a protocol parameter, but via headers/metadata.
Example Protobuf Schema
Suppose we have the following service:
1// protos/user.proto
2syntax = "proto3";
3
4service UserService {
5 rpc GetProfile(Empty) returns (UserProfile);
6}
7
8message Empty {}
9
10message UserProfile {
11 string user_id = 1;
12 string name = 2;
13 string email = 3;
14}There is no auth token field, because it will be sent through metadata.
Implementing gRPC Metadata
Let’s look at the implementation in two popular languages:
1. Node.js (using @grpc/grpc-js)
a) Client: Sending a Request Along With Metadata
1const grpc = require('@grpc/grpc-js');
2const protoLoader = require('@grpc/proto-loader');
3
4// Load proto
5const packageDefinition = protoLoader.loadSync('./protos/user.proto');
6const userProto = grpc.loadPackageDefinition(packageDefinition).UserService;
7
8// Create the client
9const client = new userProto('localhost:50051', grpc.credentials.createInsecure());
10
11// Create the metadata
12const metadata = new grpc.Metadata();
13metadata.add('authorization', 'Bearer eyJhbGciOiJI...'); // a JWT, for example
14
15// Call the RPC with metadata
16client.GetProfile({}, metadata, (err, response) => {
17 if (err) console.error(err);
18 else console.log(response);
19});b) Server: Reading the Request Metadata
1const grpc = require('@grpc/grpc-js');
2
3function getProfile(call, callback) {
4 // Get the 'authorization' value
5 const authToken = call.metadata.get('authorization')[0];
6 console.log('Received JWT:', authToken);
7
8 // TODO: Verify the token
9 // Simulate a user profile
10 if (authToken) {
11 callback(null, {
12 user_id: 'u-123',
13 name: 'Rizky',
14 email: 'rizky@example.com'
15 });
16 } else {
17 callback({
18 code: grpc.status.UNAUTHENTICATED,
19 message: 'Auth token required'
20 });
21 }
22}2. Go: Server & Client
a) Client: Sending Metadata
1import (
2 "context"
3 "google.golang.org/grpc"
4 "google.golang.org/grpc/metadata"
5)
6
7func main() {
8 conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
9 defer conn.Close()
10 client := pb.NewUserServiceClient(conn)
11
12 // Create the metadata
13 md := metadata.New(map[string]string{"authorization": "Bearer abcd1234"})
14 ctx := metadata.NewOutgoingContext(context.Background(), md)
15
16 // RPC call with metadata
17 resp, err := client.GetProfile(ctx, &pb.Empty{})
18 // Handle resp / err
19}b) Server Handler: Reading Metadata
1import (
2 "context"
3 "google.golang.org/grpc/metadata"
4)
5
6func (s *server) GetProfile(ctx context.Context, req *pb.Empty) (*pb.UserProfile, error) {
7 md, ok := metadata.FromIncomingContext(ctx)
8 if !ok {
9 return nil, status.Error(codes.Unauthenticated, "No metadata found")
10 }
11 tokens := md["authorization"]
12 if len(tokens) == 0 {
13 return nil, status.Error(codes.Unauthenticated, "No authorization token")
14 }
15 // Verify the token, etc.
16 return &pb.UserProfile{
17 UserId: "u-456",
18 Name: "Dewi",
19 Email: "dewi@example.com",
20 }, nil
21}Metadata Types: Default vs Custom
| Metadata Key | Example | Description |
|---|---|---|
authorization | Bearer eyJhbG… | JWT/OAuth2 token |
x-trace-id | 3f4a… | Correlation for tracing |
locale | id-ID | Multi-language customization |
content-type | application/grpc | Usually managed automatically |
Notes:
- Standard keys are usually all lowercase.
- Any “binary” value (not a string) must be given the
-binsuffix.
Best Practices for Using Metadata
1. Don’t Overload Metadata
Metadata is very useful, but make sure it stays small—ideally tens to hundreds of bytes. Metadata that is too large can degrade performance and break the semantics of the service.
2. Standardize Keys
Use consistent naming and document the keys used across the entire team/service.
3. Keep It One-Way at Minimum
Use different metadata for requests and responses in line with the gRPC standard.
For example, client authentication goes from client -> server, while error details go in the response.
4. Don’t Store Important State
Metadata is stateless. Don’t use it to store information that should live in a database or session.
5. Interceptor/Middleware
Implement validation, logging, or metadata injection through an interceptor/middleware. It’s modular and scalable.
Interceptor: Inject Automatically on Every Request (Node.js Example)
1function authInterceptor(options, nextCall) {
2 return new grpc.InterceptingCall(nextCall(options), {
3 start: function(metadata, listener, next) {
4 // Inject the token on every request
5 metadata.add('authorization', 'Bearer juragan_secret');
6 next(metadata, listener);
7 }
8 });
9}
10
11// Use it on the client
12const client = new userProto('localhost:50051', grpc.credentials.createInsecure(), {
13 interceptors: [authInterceptor]
14});Common Troubleshooting
- Metadata not reaching the server: Check the key spelling and the interceptor implementation, and keep in mind that oversized metadata may be rejected by middleware/network.
- Metadata lost in a stream (bidirectional): Some implementations need to pay attention to initial vs trailing metadata.
- Binary header errors: Keys with the
-binsuffix are required for buffer/binary data; otherwise you’ll run into parsing errors.
Conclusion
With a solid understanding and the best practices around metadata, your gRPC services can be more secure, traceable, and future-proof. Service-to-service communication that carries context is now a reality without having to “break” the protocol—all it takes is a touch of metadata.
Don’t forget to test edge-case scenarios and build a utility/interceptor for consistency in how metadata is sent and processed. Metadata isn’t just a “header”; it’s also a key to interoperability and observability in modern distributed systems.
Other Sources & References:
Happy coding—may your service grow ever more robust and scalable! 🚀