A gRPC Cheat Sheet for Go Servers Clients and Streams
A quick gRPC reference for Go: proto and generation, server and client skeletons, streaming loops, status codes, metadata, deadlines and settings.

A reference for the things you look up while writing gRPC in Go. Each section is short, links to the article that explains it, and uses the same names as the examples in the series. Versions: Go 1.25, grpc-go 1.75, protobuf-go 1.36.
Tested with Go 1.25.6, grpc-go v1.75.0, a reference assembled from the runnable examples in this series
How to use this sheet
It is a lookup table for people who already know why. If a section raises a question, the linked article has the experiment behind it. The snippets are condensed: they leave out imports and error handling you would add in real code, and a few use placeholder names (tlsConfig, logging, hs) for things defined elsewhere in a service.
Decisions to write down for every service
A service's gRPC behaviour is a set of choices that should live in one place, not be rediscovered in each handler. These are the ones I write into the service's README before the first deploy.
- The deadline policy. What the default client deadline is, what the server enforces if a client sends none, and how handlers derive deadlines for downstream calls.
- The error vocabulary. Which codes the service returns for which situations, which domain errors map to them, and which details (
BadRequest,ErrorInfo,RetryInfo) clients can rely on. - The retry rules. Which methods are idempotent, which codes are retried, how many attempts, and whether throttling is on.
- The authentication model. What identifies a calling service (client certificate name, token audience), what identifies a user, and which methods need which scope.
- The shutdown sequence. The drain delay, the graceful-stop budget, and the platform's kill timeout they must fit inside.
Five paragraphs cover them. Most production gRPC incidents are a disagreement between two teams about one of these five, discovered under load.
Mistakes the sheet is designed to prevent
The ones that cost the most are on this page as defaults: a deadline in the client skeleton, both unary and stream interceptor chains in the server skeleton, a connection created once, and a shutdown sequence that tells balancers before it stops listening. If you copy only those four things into a new service, you avoid the most common causes of hangs, open streaming methods, connection storms and dropped calls. The longer list of mistakes, with measurements for three of them, is in gRPC mistakes in Go services.
Schema
syntax = "proto3";
package orders.v1; // the version is part of every method path
option go_package = "example.test/gen/orders/v1;ordersv1";
service OrderService {
rpc Get(GetRequest) returns (Order); // unary
rpc List(ListRequest) returns (stream Order); // server streaming
rpc Upload(stream Chunk) returns (Summary); // client streaming
rpc Chat(stream Message) returns (stream Message); // bidirectional
}Rules: never change or reuse a field number; reserved removed numbers and names; enums start with X_UNSPECIFIED = 0; one request and one response message per rpc; add, do not alter (compatibility, protobuf basics).
Generation
protoc -I proto \
--go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
proto/orders/v1/orders.protoPin the plugins in go.mod with a tools.go (//go:build tools) and build them into bin/; regenerate in CI and fail on a diff (code generation). Lint and breaking checks: buf lint, buf breaking --against '.git#branch=main' (Buf).
Server
type server struct{ pb.UnimplementedOrderServiceServer } // always embed
func (s *server) Get(ctx context.Context, r *pb.GetRequest) (*pb.Order, error) {
if r.GetId() == "" {
return nil, status.Error(codes.InvalidArgument, "id is required")
}
return &pb.Order{Id: r.GetId()}, nil
}
lis, _ := net.Listen("tcp", ":50051")
srv := grpc.NewServer(
grpc.ChainUnaryInterceptor(logging, recovery, auth),
grpc.ChainStreamInterceptor(streamLogging, streamRecovery, streamAuth), // both, always
grpc.Creds(credentials.NewTLS(tlsConfig)),
grpc.KeepaliveParams(keepalive.ServerParameters{MaxConnectionAge: 30 * time.Minute, MaxConnectionAgeGrace: 30 * time.Second}),
grpc.MaxRecvMsgSize(8<<20),
)
pb.RegisterOrderServiceServer(srv, &server{})
healthpb.RegisterHealthServer(srv, hs)
srv.Serve(lis)Use getters (r.GetId()), which are nil-safe. Embed the unimplemented type so new rpcs do not break the build (tutorial).
Client
conn, err := grpc.NewClient("dns:///orders.internal:50051",
grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
grpc.WithDefaultServiceConfig(`{"loadBalancingConfig":[{"round_robin":{}}]}`),
grpc.WithChainUnaryInterceptor(withDeadline(5*time.Second), propagateRequestID),
)
defer conn.Close() // one per target, for the life of the process
c := pb.NewOrderServiceClient(conn)
ctx, cancel := context.WithTimeout(ctx, 2*time.Second) // every call has a deadline
defer cancel()
o, err := c.Get(ctx, &pb.GetRequest{Id: "o1"})NewClient connects lazily and defaults to the dns resolver. Use passthrough:///host:port for a literal address, insecure.NewCredentials() only in tests (load balancing, authentication).
Streaming loops
// server streaming, client side
stream, _ := c.List(ctx, &pb.ListRequest{})
for {
o, err := stream.Recv()
if errors.Is(err, io.EOF) { break } // clean end
if err != nil { return err } // a real failure
use(o)
}
// server streaming, server side
for _, o := range items {
if err := stream.Send(o); err != nil { return err } // client gone or deadline passed
}
// client streaming
up, _ := c.Upload(ctx)
for _, ch := range chunks { up.Send(ch) }
sum, err := up.CloseAndRecv()
// bidirectional: send and receive on separate goroutines, CloseSend() when done sendingWhen Send returns an error, call Recv for the real status. One goroutine per direction; never two senders (server streaming, bidirectional).
Status codes
| Situation | Code |
|---|---|
| Bad input, whatever the state | InvalidArgument |
| Wrong system state for the call | FailedPrecondition |
| Missing, or hidden from the caller | NotFound |
| Duplicate create | AlreadyExists |
| No or bad credentials / not allowed | Unauthenticated / PermissionDenied |
| Quota or rate limit | ResourceExhausted |
| Transient, safe to retry | Unavailable |
| Optimistic concurrency conflict | Aborted |
| Time limit passed | DeadlineExceeded |
| Bug | Internal (generic message) |
st := status.New(codes.InvalidArgument, "the request is invalid")
st, _ = st.WithDetails(&errdetails.BadRequest{FieldViolations: violations})
return nil, st.Err()
// client
code := status.Code(err); st := status.Convert(err); details := st.Details()Map domain errors in one place; unconverted errors reach the client as Unknown with their text (error codes).
Metadata
ctx = metadata.AppendToOutgoingContext(ctx, "x-request-id", id) // client to server
md, _ := metadata.FromIncomingContext(ctx) // in a handler
grpc.SetHeader(ctx, metadata.Pairs("k", "v")); grpc.SetTrailer(ctx, md) // server to client
c.Get(ctx, req, grpc.Header(&hdr), grpc.Trailer(&trl)) // client reads themKeys are lower-case; -bin keys carry binary; grpc- is reserved. Incoming metadata is not forwarded automatically: copy an allow-list (metadata).
Deadlines, retries, keepalive
ctx, cancel := context.WithTimeout(parent, 2*time.Second) // pass the handler's ctx down: the budget shrinks{"methodConfig":[{"name":[{"service":"orders.v1.OrderService"}],
"retryPolicy":{"maxAttempts":3,"initialBackoff":"0.1s","maxBackoff":"1s","backoffMultiplier":2,
"retryableStatusCodes":["UNAVAILABLE"]}}],
"retryThrottling":{"maxTokens":10,"tokenRatio":0.1}}Client keepalive Time is at least 10 s and must be at least the server's EnforcementPolicy.MinTime (default 5 minutes) or the server sends too_many_pings. Hedging is not implemented in grpc-go (deadlines, retries, keepalive).
Limits
| Limit | Default | Option |
|---|---|---|
| Server receive | 4 MiB | grpc.MaxRecvMsgSize |
| Client receive | 4 MiB | grpc.MaxCallRecvMsgSize |
| Send (both) | unlimited | MaxSendMsgSize, MaxCallSendMsgSize |
| Metadata total | about 8 KiB | grpc.MaxHeaderListSize |
Stream big payloads instead of raising limits (message size).
Shutdown and health
hs.Shutdown() // NOT_SERVING: balancers stop sending
time.Sleep(drain) // let that propagate
go func() { srv.GracefulStop(); close(done) }()
select { case <-done: case <-time.After(budget): srv.Stop() }(graceful shutdown, health and reflection).
Testing
lis := bufconn.Listen(1 << 20) // in-memory listener
conn, _ := grpc.NewClient("passthrough:///bufnet",
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { return lis.DialContext(ctx) }),
grpc.WithTransportCredentials(insecure.NewCredentials()))Table-test status.Code(err); read streams to the end; check goroutines after stream tests (testing).
Tools
| Command | Use |
|---|---|
grpcurl -plaintext localhost:50051 list | Services on a server with reflection |
grpcurl -d '{"id":"o1"}' host:port orders.v1.OrderService/Get | Call a method with JSON |
buf lint, buf breaking, buf generate | Schema checks and generation |
GRPC_GO_LOG_SEVERITY_LEVEL=info | Connection and transport logs |
The reference documentation is at grpc.io and pkg.go.dev/google.golang.org/grpc, and the protobuf language guide at protobuf.dev.


