Skip to content

Day 118 — Deadlines, Metadata & Status Codes

Month 5 · Week 1 · ⬅ Day 117 · Day 119 ➡ · Journal index

🎯 Learning Objective

Use the three cross-cutting concerns of every RPC correctly: propagate deadlines via context, attach/read per-call metadata, and return/inspect rich status-code errors.

📚 Topics

  • Deadlines/timeouts with context.WithTimeout, grpc-timeout, DeadlineExceeded
  • metadata.MD (outgoing/incoming), status/codes, status details

📖 Reading / Sources

📝 Notes

  • A deadline is absolute, a timeout is relative; the client sets one and gRPC ships it downstream as the grpc-timeout header. Every hop shares the same absolute deadline, so always derive child contexts, never reset them → [[context]].
  • When the deadline passes, in-flight work is cancelled and the call ends with codes.DeadlineExceeded; the server should poll ctx.Err() and bail early instead of doing doomed work → [[deadlines]].
  • Metadata is metadata.MD = map[string][]string with case-insensitive keys. Client attaches it with metadata.NewOutgoingContext; server reads it with metadata.FromIncomingContext. Binary values use a -bin key suffix → [[metadata]].
  • Reserved grpc-* headers (grpc-timeout, grpc-status, ...) are managed by the framework — don't set them by hand. Use metadata for app concerns like authorization.
  • Status = (codes.Code, message, details). Code 0 is OK; everything else is an error. Build with status.New(code, msg)/status.Error(code, msg); inspect on the client with status.FromError(err) then st.Code() → [[status-codes]].
  • Server-side cancellation vs. client-side: context.Canceled (client hung up) is distinct from DeadlineExceeded (ran out of time). Map your domain errors to the right codes.X so callers can retry intelligently (Unavailable is retryable; InvalidArgument is not).
  • gRPC codes have a canonical HTTP mapping (used by grpc-gateway): e.g. NotFound→404, InvalidArgument→400, PermissionDenied→403, Unauthenticated→401, Unavailable→503.

💻 Code Examples

// Client: deadline + metadata on the outgoing call.
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs("authorization", "Bearer abc123"))

u, err := client.GetUser(ctx, &userv1.GetUserRequest{Id: 1})
if st, ok := status.FromError(err); ok && st.Code() == codes.DeadlineExceeded {
    log.Println("call timed out")
}
// Server: read metadata, honor the deadline, return a typed status error.
func (s *server) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.User, error) {
    md, _ := metadata.FromIncomingContext(ctx)
    if len(md.Get("authorization")) == 0 { // keys are case-insensitive
        return nil, status.Error(codes.Unauthenticated, "missing token")
    }
    if ctx.Err() != nil { // client cancelled or deadline blew
        return nil, status.FromContextError(ctx.Err()).Err()
    }
    u, ok := s.find(req.GetId())
    if !ok {
        return nil, status.Errorf(codes.NotFound, "user %d not found", req.GetId())
    }
    return u, nil
}

Deadline propagation rebuilt with context: examples/month-05/deadline (go run ./examples/month-05/deadline) · the status-code model + HTTP mapping: examples/month-05/statuscodes (go run ./examples/month-05/statuscodes) · metadata + auth: examples/month-05/authmeta (go run ./examples/month-05/authmeta)

🏋️ Exercises / Practice

Exercise Status Link
Implement the Code/Status model + HTTPStatus mapping exercises/month-05/week-1/statuscode

🐛 Mistakes Made

  • Looked up a metadata key with the exact case I sent it (Authorization) and missed it — metadata.MD keys are lower-cased; use md.Get which is case-insensitive.
  • Created a fresh context.Background() inside a handler for a downstream call, dropping the inherited deadline. Always derive from the incoming ctx.

❓ Open Questions

  • How much deadline "budget" should each hop reserve so the innermost call still has time? (Pattern: subtract a small per-hop margin, or set per-service timeouts that sum under the client's.)

🧠 Active Recall (answer without looking)

  1. Q: A handler creates context.Background() to call a downstream service. What did it just break?
    A

It dropped the incoming deadline, cancellation, and metadata. Downstream work no longer stops when the original caller's deadline expires or it cancels. Always derive the child context from the handler's ctx. 2. Q: Why prefer status.Error(codes.NotFound, ...) over errors.New("not found") from a handler?

A

The plain error reaches the client as codes.Unknown, losing semantics. A typed status lets the caller branch on st.Code() (retry Unavailable, don't retry InvalidArgument) and maps cleanly to an HTTP status at a gateway.

🪶 Feynman Reflection

Every RPC carries three side-channels besides its payload: a deadline (when to give up), metadata (key/value headers like auth), and a status (a numeric code + message describing the outcome). The deadline rides the context, metadata lives in the context too, and the status replaces Go's free-form error with a small fixed vocabulary every language shares.

🕳️ Knowledge Gaps

  • Rich error details (status.WithDetails, errdetails protos) for structured failures.
  • Trailing metadata vs. header metadata timing on streams.

✅ Summary

I can set and propagate deadlines, attach and read case-insensitive metadata, and return/inspect status-code errors with the correct HTTP mapping.

⏭️ Next Steps / Prep for Tomorrow

  • Day 119: consolidate the week, re-quiz, and write the review.

Time spent Difficulty Confidence
90 min 🟦🟦⬜⬜⬜ 🟦🟦🟦⬜⬜

Suggested commit: docs(journal): deadlines, metadata and status codes (day 118)