gRPC in Go: Contracts, Streaming, and Performance
Microservices need a contract that survives refactors, a wire format that stays compact, and a transport that multiplexes many calls on one connection.
gRPC answers all three by pairing Protocol Buffers schemas with HTTP/2 framing and a small set of RPC patterns.
Go's google.golang.org/grpc package is the de facto server and client stack for internal platforms.
gRPC Basics walks runnable setup; sibling pages cover schema design, streaming, interceptors, errors, gateways, and operations.
Summary
- gRPC is a remote procedure call framework where
.protofiles define services and messages, code generators emit typed Go APIs, and HTTP/2 carries length-prefixed protobuf frames between processes. - Insight: Teams stop debating JSON field names in Slack; compilers catch breaking API changes; binary payloads and header compression reduce bandwidth; streaming RPCs model live feeds without inventing WebSocket protocols per service.
- Key Concepts: service, unary RPC, streaming RPC, stub, status code, metadata, deadline, HTTP/2.
- When to Use: Service-to-service calls inside a VPC or mesh, high-throughput event pipelines, polyglot fleets that share one proto repo, and platforms that need bidirectional streams (logs, replication, gaming).
- Limitations/Trade-offs: Browser clients need grpc-web or a JSON gateway; human debugging is harder than curl; load balancers must understand long-lived HTTP/2 streams; proto evolution rules constrain schema changes.
- Related Topics: REST gateways, OpenTelemetry tracing, Kubernetes headless services, protobuf backward compatibility, TLS/mTLS.
Foundations
Think of gRPC as "functions across the network."
You declare a service in a .proto file:
service Inventory {
rpc GetStock(GetStockRequest) returns (GetStockResponse);
}protoc with protoc-gen-go and protoc-gen-go-grpc emits Go interfaces such as InventoryServer and client stubs.
The server implements the interface; the client calls client.GetStock(ctx, req).
Under the hood, each call becomes an HTTP/2 stream carrying protobuf-encoded request and response messages.
Unlike REST, the path and verb are fixed by the RPC name - you do not design URLs per endpoint.
Unary RPCs are request-response, like a function call.
Server streaming sends many responses to one request (tail logs, search results).
Client streaming uploads many requests before one response (bulk ingest).
Bidirectional streaming keeps both sides open (chat, replication).
Metadata (key-value headers) travels outside the message body for auth tokens, trace IDs, and tenancy hints.
Mechanics & Interactions
A typical Go server registers implementations on a grpc.Server, listens on TCP, and serves HTTP/2:
Client stub Server
│ │
│── HTTP/2 stream ───────►│ interceptor chain
│ (protobuf frames) │ │
│ │ handler method
│◄── status + message ────│
Deadlines ride on context.Context.
When a client sets context.WithTimeout, the server observes ctx.Done() and should stop work.
Status returns as a gRPC code (codes.NotFound, codes.InvalidArgument, …) plus a message and optional details protobufs for structured errors.
The Go stack uses HTTP/2 flow control so one massive stream does not starve others on the same connection.
Keepalive settings matter for NATs and L4 load balancers that silently drop idle TCP.
Performance wins versus JSON REST come from smaller payloads, no repeated HTTP headers per call (HPACK), and optional gzip compression for large messages.
Profiling still matters: reflection, allocation-heavy protobuf merges, and interceptor chains add overhead.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| gRPC + protobuf | Strong contracts, fast binary wire | Needs codegen pipeline | Internal microservices |
| REST + JSON | Universal tooling, easy debugging | Weak typing, larger payloads | Public HTTP APIs, browsers |
| gRPC-Gateway | One proto, JSON clients | Extra hop, mapping limits | Mobile/BFF layers |
| Connect/gRPC-Web | Browser-friendly variants | Ecosystem fragmentation | SPA talking to gRPC backends |
| Message queues | Decoupled async | Higher latency, ordering complexity | Fire-and-forget events |
Advanced Considerations & Applications
In Kubernetes, headless Services or L7 proxies (Envoy, Linkerd, Istio) route gRPC by HTTP/2 authority and :path.
L4 balancers that rotate TCP per request break streaming unless they are gRPC-aware.
mTLS and JWT metadata are standard patterns; interceptors centralize auth before handlers run.
OpenTelemetry gRPC stats handlers emit spans per RPC; pair with metadata propagation (traceparent).
Proto versioning (v1, v2 packages) lets fleets migrate without flag days.
Breaking changes (renumbering fields, changing types) are deployment hazards - treat .proto like database migrations.
controller-runtime and operator ecosystems increasingly expose gRPC alongside REST for admission and extension APIs.
For public edges, teams front gRPC services with grpc-gateway or BFF layers that speak JSON while core services stay protobuf-native.
Watch GOMAXPROCS, connection pool sizing, and max concurrent streams when one client fans out to thousands of backends.
Common Misconceptions
- "gRPC is always faster than REST" - For tiny payloads the difference may be noise; JSON wins on human iteration speed during early prototyping.
- "Protobuf means encrypted" - gRPC runs over TLS when you configure credentials; plaintext gRPC is common in dev only.
- "Streaming RPCs are just chunked HTTP" - Flow control, half-close semantics, and status trailers differ; clients must handle
io.EOFand error trailers correctly. - "One proto file per microservice" - Shared message types and imported protos reduce duplication; monolithic proto repos need strict ownership.
- "Browsers speak gRPC natively" - They do not; plan grpc-web, Connect, or a gateway before promising browser clients direct access.
FAQs
What problem does gRPC solve that REST does not?
Strong API contracts via protobuf, first-class streaming, and efficient HTTP/2 multiplexing.
REST remains better for ad hoc HTTP caching and browser-first public APIs.
Do I still write HTTP handlers in Go?
gRPC handlers implement generated interfaces, not http.HandlerFunc.
You can mount gRPC and REST on different ports or use gateways to expose both.
How does versioning work?
Add fields with new numbers; never reuse numbers.
Publish new packages (v2) for incompatible changes and run both during migration.
Where do deadlines live?
On context.Context created by the client.
Servers should respect ctx.Done() and return codes.Canceled or codes.DeadlineExceeded.
Is protobuf required?
gRPC's canonical IDL is protobuf.
Other serializers exist in other ecosystems; Go production stacks assume protobuf.
How do I debug a failing RPC?
Log status codes, enable grpc debug logging, and use grpcurl or BloomRPC against reflection-enabled servers.
Structured error details beat plain text messages.
When should I pick streaming over unary?
When the server produces an unbounded sequence, the client uploads chunks, or both sides need concurrent writes.
Unary stays simpler for CRUD.
Does gRPC replace message queues?
No - queues decouple producers and consumers in time.
gRPC suits synchronous request-response and controlled streams between known peers.
What breaks behind naive load balancers?
Long-lived HTTP/2 connections and bidi streams need connection-aware routing or proxyless service mesh configs.
How does this relate to net/http?
gRPC sits on HTTP/2 but not on ServeMux patterns.
Many teams run chi/gin/echo for public HTTP and gRPC for internal RPC on separate listeners.
Related
- gRPC Basics - runnable unary server setup
- Protocol Buffers Schema Design - field rules and evolution
- Unary, Streaming & Bidirectional RPCs - four RPC shapes
- gRPC Error Codes & Status Mapping - status handling
- REST vs gRPC Trade-off Guide - when to pick each
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).