13 - Metadata & Deadlines
📋 Jump to TakeawaysMetadata is gRPC's equivalent of HTTP headers. Deadlines are timeouts that propagate across service boundaries. Together they let you pass context information and prevent cascading failures.
Metadata
Metadata is a set of key-value pairs sent alongside RPC calls. Keys are strings, values are strings or binary. Use it for auth tokens, request IDs, tracing headers, or any data that doesn't belong in the request message.
Note: You already used metadata in lesson 11 — the auth interceptor reads
metadata.FromIncomingContext(ctx)to check the authorization header. This lesson covers metadata in full.
Sending Metadata from the Client
import "google.golang.org/grpc/metadata"
ctx := context.Background()
md := metadata.New(map[string]string{
"authorization": "Bearer my-token",
"x-request-id": "req-12345",
})
ctx = metadata.NewOutgoingContext(ctx, md)
resp, err := client.GetLink(ctx, &pb.GetLinkRequest{ShortCode: "abc123"})Or append to existing metadata:
ctx = metadata.AppendToOutgoingContext(ctx,
"authorization", "Bearer my-token",
"x-request-id", "req-12345",
)Reading Metadata on the Server
func (s *linkServer) GetLink(ctx context.Context, req *pb.GetLinkRequest) (*pb.GetLinkResponse, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Internal, "missing metadata")
}
requestID := ""
if vals := md.Get("x-request-id"); len(vals) > 0 {
requestID = vals[0]
}
log.Printf("request_id=%s method=GetLink code=%s", requestID, req.GetShortCode())
// ... handle the request
}md.Get() returns a slice because a key can have multiple values. Always check the length before accessing.
Sending Metadata from the Server
The server can send metadata back in two ways: headers (sent before the response) and trailers (sent after).
- Headers — arrive at the client before the response data. Use for: server identity, routing info, cache hints. Call
grpc.SendHeader(ctx, md). - Trailers — arrive after the response is complete. Use for: processing time, checksums, anything you only know after doing the work. Call
grpc.SetTrailer(ctx, md).
For unary RPCs the difference is subtle since everything arrives together. For streaming it matters more — headers arrive when the stream opens, trailers arrive when the stream closes.
func (s *linkServer) GetLink(ctx context.Context, req *pb.GetLinkRequest) (*pb.GetLinkResponse, error) {
// Send header metadata
header := metadata.Pairs("x-served-by", "node-1")
grpc.SendHeader(ctx, header)
// ... do work ...
// Send trailer metadata
trailer := metadata.Pairs("x-processing-time", "12ms")
grpc.SetTrailer(ctx, trailer)
return &pb.GetLinkResponse{Link: link}, nil
}Reading Server Metadata on the Client
var header, trailer metadata.MD
resp, err := client.GetLink(ctx, req,
grpc.Header(&header),
grpc.Trailer(&trailer),
)
fmt.Println("served by:", header.Get("x-served-by"))
fmt.Println("processing time:", trailer.Get("x-processing-time"))Deadlines
A deadline is an absolute point in time by which an RPC must complete. If the deadline passes, the call fails with codes.DeadlineExceeded on both the client and server.
Setting a Deadline
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
resp, err := client.CreateLink(ctx, req)
if err != nil {
st, _ := status.FromError(err)
if st.Code() == codes.DeadlineExceeded {
log.Println("request timed out")
}
}context.WithTimeout creates a deadline 3 seconds from now. context.WithDeadline lets you set an absolute time.
Deadline Propagation
This is the important part. When service A calls service B with a 5-second deadline, and service B calls service C, the deadline propagates automatically. If 3 seconds have already passed by the time B calls C, C only has 2 seconds left.
Client (5s deadline)
→ Service A (5s remaining)
→ Service B (3s remaining, 2s already used)
→ Service C (1s remaining, 4s already used)You don't have to calculate remaining time yourself. Just pass the context through:
// In service B's handler
func (s *server) SomeMethod(ctx context.Context, req *pb.Request) (*pb.Response, error) {
// ctx already has the deadline from the caller
// Pass it to downstream calls
resp, err := serviceC.AnotherMethod(ctx, downstreamReq)
// ...
}Checking Deadline on the Server
func (s *linkServer) CreateLink(ctx context.Context, req *pb.CreateLinkRequest) (*pb.CreateLinkResponse, error) {
// Check if we still have time
if ctx.Err() == context.DeadlineExceeded {
return nil, status.Error(codes.DeadlineExceeded, "deadline already passed")
}
// For long operations, check periodically
select {
case <-ctx.Done():
return nil, status.Error(codes.DeadlineExceeded, "deadline exceeded during processing")
default:
// continue processing
}
// ... create the link
}Cancellation
Clients can cancel requests at any time. The server sees this through the context:
// Client
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(1 * time.Second)
cancel() // cancel the request after 1 second
}()
resp, err := client.ListLinks(ctx, req)On the server, ctx.Done() is closed when the client cancels. Long-running operations should check for cancellation:
func (s *linkServer) ListLinks(req *pb.ListLinksRequest, stream pb.LinkService_ListLinksServer) error {
for _, link := range s.links {
select {
case <-stream.Context().Done():
return status.Error(codes.Cancelled, "client cancelled")
default:
}
if err := stream.Send(link); err != nil {
return err
}
}
return nil
}Key Takeaways
- Metadata is key-value pairs sent with RPCs, like HTTP headers
- Use
metadata.NewOutgoingContexton the client,metadata.FromIncomingContexton the server - Servers can send metadata back as headers (before response) or trailers (after response)
- Deadlines propagate automatically across service boundaries through context
- Always set a deadline on client calls to prevent hanging forever
- Check
ctx.Done()in long-running server operations to respect cancellation codes.DeadlineExceededandcodes.Cancelledare the relevant status codes