gRPC vs REST vs GraphQL: Notes From Three Different Teams' Choices
Each team chose a different API style and each has regrets. An honest retrospective on the trade-offs that emerged in practice rather than in architecture diagrams.

Three teams, three API styles, three sets of regrets that only showed up after eighteen months of production traffic. This is not a winner-take-all comparison — it is a ship log from a platform org that inherited all three simultaneously when we acquired a smaller company and never consolidated.
What each team chose and why
Payments (gRPC + protobuf 3.21): The original architects came from a Google-adjacent background. They wanted strongly typed contracts, bi-directional streaming for settlement reconciliation, and HTTP/2 multiplexing on the internal mesh. Their .proto files live in a monorepo; buf lint runs in CI. Services run on Go 1.22 with google.golang.org/grpc v1.63.
Catalog (REST + OpenAPI 3.1): Public-facing, partner-integrated, cache-friendly. JSON over HTTPS, CDN in front of read endpoints, ETag/If-None-Match on product detail. Spring Boot 3.2 on the write path; FastAPI 0.110 on a read replica service we spun up for latency.
Recommendations (GraphQL + Apollo Server 4): Mobile clients wanted to fetch a home feed, user prefs, and three recommendation carousels in one round trip. The team argued that REST would produce either over-fetching or six sequential calls on 3G. They shipped a federated subgraph pattern before we had federation governance.
Findings after production load
gRPC: wins on the inside, friction at the edge
Internal service-to-service latency dropped measurably once we moved off REST-with-JSON between payments microservices. p50 dropped from ~8 ms to ~3 ms on identical hardware — mostly serialization and connection reuse, not magic.
The regrets:
- Browser clients. We ended up maintaining grpc-web + Envoy transcoding for one admin dashboard. That stack is its own on-call rotation.
- Debugging.
grpcurlhelps, but support engineers still ask for curl examples. We now auto-generate OpenAPI from protos for read-only endpoints — duplicate contract maintenance we swore we would avoid. - Version skew. A team shipped a field rename without a
reservedblock. Old clients silently ignored the new field for two weeks. We added breaking-change detection inbuf breakingagainstmain.
REST: boring in the best and worst ways
REST won on operability. Every SRE knows how to trace an HTTP request. CloudFront cache hit ratio on catalog reads sits at 94% for product detail pages. Partner onboarding is "here is the OpenAPI spec."
The regrets:
- Endpoint sprawl. We have 47 GET routes for catalog variants that differ only in included relations. Adding a field to the default response broke a partner who parsed strictly. We now version URLs (
/v2/products) and hate ourselves a little. - N+1 on the write side. Hibernate lazy-loading caused a postmortem-worthy incident when a bulk import fans out to 200 queries per row. We fixed with explicit fetch joins, not by switching protocols.
GraphQL: flexibility tax
The recommendations subgraph is beloved by mobile. One query, shaped payloads, reduced battery drain on cold start — real wins, measured.
The regrets hit harder:
- Query complexity explosions. A well-meaning client requested nested recommendations with
limit: 100at three levels. PostgreSQL 15 behind the subgraph pegged CPU. We addedgraphql-query-complexitylimits and persisted-query allowlists for production mobile builds. - Caching is a design project. CDN caching GraphQL is possible but we never prioritized it. Every home feed load hits origin unless the client uses APQ (automatic persisted queries) — which only iOS adopted.
- Schema federation drift. Two subgraphs defined conflicting
Usertypes. Apollo Router 1.45 caught it in CI, but only after a staging deploy failed. Federation is an organizational problem wearing a protocol hat.
Tradeoff table (our actual criteria)
| Criterion | gRPC | REST | GraphQL |
|---|---|---|---|
| Public partner API | Poor fit without gateway | Good | Risky without guardrails |
| Mobile payload shaping | Poor | Mediocre | Good |
| Internal east-west latency | Good | OK | N/A for us |
| Observability tooling maturity | Improving | Excellent | Good with Apollo Studio |
| Contract evolution discipline | Strong if you enforce buf/protolint | Medium (OpenAPI diff) | Weak unless federated governance |
| Hiring/onboarding | Steeper | Flat | Medium |
What I would do next
If I were consolidating today — we are not, budget says live with heterogeneity until 2027 — I would:
- gRPC for internal synchronous RPC where both sides are ours and streaming matters.
- REST for anything a human or partner debugs with curl or that sits behind a CDN.
- GraphQL only behind a BFF with persisted queries, complexity limits, and no ad-hoc public introspection.
The mistake was treating the choice as permanent architecture rather than per-boundary context. Our RFC process now requires an explicit "API style justification" section with expected QPS, client types, and caching strategy — not a default based on whatever the tech lead used at their last job.
Database index bloat from GraphQL resolver patterns is a separate post; see Postgres index bloat in production for the query-shape side of that pain.
What I would not do
- Mandate one protocol org-wide to "reduce complexity." You will run a gateway that speaks all three anyway.
- Choose GraphQL because REST feels unfashionable. Measure round trips and payload sizes on real devices first.
- Skip code generation on gRPC. Hand-written stubs drift.
Cross-team governance we added late
Six months post-acquisition we stood up an API Review office hours — not approval gate, office hours. Required attendance when:
- New public REST surface >5 endpoints
- New gRPC service exported outside team boundary
- GraphQL schema change affecting federated
UserorAccounttypes
Review checklist includes caching strategy, pagination defaults, and idempotency keys. Reduced partner-breaking changes from ~2/quarter to ~0.5/quarter — not because REST won, because someone asked the questions earlier.
Observability portability
Each stack exported different golden signals initially. Consolidation to OpenTelemetry helped compare apples-to-apples — but protocol choice still affects what you can trace (gRPC metadata vs HTTP headers vs GraphQL field paths). Standardized on W3C trace context propagation everywhere; baggage still team-specific.
Three teams, three reasonable choices, three different failure modes. That is the honest retrospective.
Manish Bookreader
Electronics enthusiast, Embedded Systems Expert, Linux/Networking programmer, and Software Engineer passionate about AI, electronics, books, and cooking.

