Home/Collections/Software Engineering/gRPC vs REST vs GraphQL: Notes From Three Different Teams' Choices
article
October 17, 2022
34 min read
78 views

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.

gRPC vs REST vs GraphQL: Notes From Three Different Teams' Choices

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. grpcurl helps, 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 reserved block. Old clients silently ignored the new field for two weeks. We added breaking-change detection in buf breaking against main.

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: 100 at three levels. PostgreSQL 15 behind the subgraph pegged CPU. We added graphql-query-complexity limits 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 User types. 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)

CriteriongRPCRESTGraphQL
Public partner APIPoor fit without gatewayGoodRisky without guardrails
Mobile payload shapingPoorMediocreGood
Internal east-west latencyGoodOKN/A for us
Observability tooling maturityImprovingExcellentGood with Apollo Studio
Contract evolution disciplineStrong if you enforce buf/protolintMedium (OpenAPI diff)Weak unless federated governance
Hiring/onboardingSteeperFlatMedium

What I would do next

If I were consolidating today — we are not, budget says live with heterogeneity until 2027 — I would:

  1. gRPC for internal synchronous RPC where both sides are ours and streaming matters.
  2. REST for anything a human or partner debugs with curl or that sits behind a CDN.
  3. 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 User or Account types

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.

You Might Also Like

Mini Self-Balancing Robot
Electronics

Mini Self-Balancing Robot

A miniature two wheeled self balancing robot using a XIAO ESP32C3, an MPU6050 gyro/accelerometer, and 3V gear motors.

Apple’s iPhone 18 Pro Features: Launching
Tech News

Apple’s iPhone 18 Pro Features: Launching

Apple's September 2026 iPhone 18 Pro drops the flashiness for smarter fundamentals: a 2nm A20 chip that powers true AI features, variable aperture cameras that rival DSLRs, a Dynamic Island cut nearly in half, and batteries pushing 5,200mAh. It's incremental on paper, but the pieces add up to exactly what people actually want from their phones.

Keyestudio Stone Thrower
Electronics

Keyestudio Stone Thrower

A STEM kit for building a small catapult (stone thrower) that can be controlled by a Micro:bit or ESP32 board, using servos and sensors.