Why We Replaced REST with gRPC for Internal Microservices

Khanh Nguyen
Khanh Nguyen
(Updated: )
Listen to this article0 / 0
Split-panel hand-drawn illustration contrasting a clogged request queue with a smooth, high-speed multiplexed stream.

Internal service-to-service calls don't need a human-readable format or a browser-friendly URL — they need to move data between two machines you control as fast as possible. That's part of why a growing number of backend teams keep the public API on REST and move traffic behind the gateway to gRPC and Protocol Buffers. The rest of the reason is easy to get wrong, because most comparisons bundle two independent changes — encoding format and transport protocol — into one number.

Protobuf's Binary Encoding Cut a Trading API's Payload From 315 Bytes to 75

The easiest way to understand the difference is to think of REST/JSON as mailing a form where every field is hand-labeled — "customer_id": 4821 — versus gRPC/Protobuf as a pre-agreed shorthand where both sides already know field three means customer ID, so you just write the number. No labels, no punctuation, no repeated text.

In a documented example from an API serving trading-agent order data, a typical order response serialized to 280–350 bytes as JSON, while the same data as protobuf came in at 60–90 bytes — roughly a 3–4x reduction. A separate scaling study running the same benchmark suite at higher load found the protobuf payload was about a third the size of the equivalent JSON payload. A 2026 comparison of API protocols reported binary protobuf payloads running 3–11x smaller than JSON for the same underlying data, consistent with both specific figures above.

None of this matters much at low volume. It matters when a service makes dozens of internal calls per user request, because every byte saved is bandwidth and CPU you don't spend twice — once encoding, once decoding. This part of the case for gRPC is clean: it's purely about encoding, with no transport protocol involved yet.

Same order response: JSON vs Protobuf payload sizeA trading-API order response serialized as JSON runs roughly 315 bytes; the same data as Protobuf runs roughly 75 bytes.Same Order Response: JSON vs Protobuf Payload SizeTrading-API order response, per dev.to benchmark (2026)JSON (REST)~315 bytesProtobuf (gRPC)~75 bytes0100200300 bytesSource: dev.to — gRPC vs REST for AI Trading Agents (2026)

grpc.io's Own Benchmarks Show Protobuf Isn't Uniformly Faster

Protobuf isn't faster at everything, and this is where most comparison posts stop being careful. gRPC's own mobile benchmarking team measured serialization and deserialization separately and found protobuf consistently about 3x faster than JSON at serializing, regardless of message size. For deserialization, the picture flips at small sizes: the same team reported JSON about 1.5x faster than protobuf for messages under 1KB. Protobuf only pulled ahead on deserialization once messages passed roughly 15KB, where it ran about 2x faster. When JSON is gzipped before sending, protobuf's serialization advantage widened to more than 5x, regardless of message size.

The practical read for a solution architect: if internal calls are small — a single lookup response under 1KB — and JSON isn't being gzipped, the CPU-time case for switching is weaker than most write-ups suggest. The case strengthens once messages are larger, calls are frequent, or JSON is already compressed on the wire, since at that point you're paying compression CPU cost regardless and protobuf's binary format wins outright.

Protobuf vs JSON relative speed by operationProtobuf serialization is about 3x faster than JSON at any size and over 5x faster than gzipped JSON, but JSON deserializes about 1.5x faster than protobuf for messages under 1KB.1.0 ParityProtobuf vs JSON: Relative Speed by Operation1.0 = parity; above 1.0 protobuf faster, below 1.0 JSON faster0.01.02.04.06.03.0x fasterSerialize (any size)0.67x (JSON faster)Deserialize <1KB2.0x fasterDeserialize >15KB5.0x+ fasterSerialize (gzipped JSON)Source: grpc.io — Mobile Benchmarks (gRPC official blog)

HTTP/2 Multiplexing, Not REST Itself, Explains Most of the Queuing Penalty

Here's the confound most gRPC-vs-REST comparisons don't name: REST is an architectural style, not a transport protocol. A REST API can run over HTTP/1.1, HTTP/2, or HTTP/3 depending on how the server and gateway are configured. gRPC, by contrast, requires HTTP/2 — multiplexed streams over a single TCP connection are part of the gRPC specification, not an optional upgrade.

That means the comparison that actually matters splits into two independent questions: HTTP/1.1 versus HTTP/2 (transport), and JSON versus Protobuf (encoding). Many REST deployments still default to HTTP/1.1, simply because nothing about REST tooling forces the upgrade the way adopting gRPC does. Under HTTP/1.1, a client typically holds a limited number of parallel TCP connections per host — commonly capped around six — and once every connection is busy, additional requests wait for one to free up. If a REST service has already been moved to HTTP/2, that queuing disadvantage mostly disappears, and what's left is the smaller, cleaner gap that Protobuf's binary encoding and gRPC's generated stubs actually contribute.

The diagram below shows one specific scenario — a connection pool already at capacity — not a claim that HTTP/1.1 is inherently single-threaded. Real HTTP/1.1 clients commonly run several connections in parallel; the queuing shown here is what happens once that pool runs out, which is a real and common failure mode under high internal call volume, but not the protocol's only possible behavior.

HTTP/1.1 connection-pool exhaustion vs HTTP/2 multiplexingOne scenario: an HTTP/1.1 client whose connection pool is already at capacity must queue additional requests, while HTTP/2 multiplexes multiple streams over a single connection with no queuing. This is not HTTP/1.1's only possible behavior — real clients commonly hold several parallel connections. Conceptual diagram, not to scale.HTTP/1.1 Pool Exhaustion vs HTTP/2 MultiplexingOne scenario, not universal HTTP/1.1 behavior — not to scaleHTTP/1.1, connection pool at capacityRequest 1Request 2 (waiting)Request 3 (waiting)All connections in the pool are already in useHTTP/2 (required by gRPC)Single HTTP/2 ConnectionStream 1: Request 1Stream 2: Request 2Stream 3: Request 3All three requests travel concurrently — no queuing, by protocol designDiagram based on the HTTP/1.1 and HTTP/2 protocol specifications

Here's roughly what a service definition looks like — the .proto file is the contract both client and server generate code from, replacing hand-written REST request/response schemas:

PROTOBUF
syntax = "proto3";

package orders.v1;

service OrderService {
  rpc GetOrder (GetOrderRequest) returns (Order);
}

message GetOrderRequest {
  string order_id = 1;
}

message Order {
  string order_id = 1;
  string customer_id = 2;
  repeated LineItem items = 3;
  int64 total_cents = 4;
}

message LineItem {
  string product_id = 1;
  int32 quantity = 2;
  int64 unit_price_cents = 3;
}

A minimal internal rollout usually looks like: define the .proto contracts, generate server and client stubs per language, run the gRPC server behind the same container orchestration already in use, and put an Envoy or similar gRPC-aware proxy at the mesh edge if any callers still need REST or gRPC-Web. Note that this migration path bundles the transport upgrade in automatically — you can't adopt gRPC without also moving to HTTP/2.

A Production Migration's 340ms-to-47ms Drop Bundles Two Changes, Not One

Benchmarks explain why a change works; a production number is what convinces an engineering lead. One case cited by an infrastructure consultancy describes a critical internal data pipeline where p99 latency dropped from 340ms to 47ms — roughly 7x — after moving from JSON-over-REST to Protobuf-over-gRPC, with no other architectural changes reported alongside it.

That number should be read carefully: the migration it describes bundles an encoding change (JSON to Protobuf) with a mandatory transport change (HTTP/1.1 to HTTP/2, since gRPC requires it). Neither the source write-up nor the payload-size math above can isolate how much of the 7x drop came from smaller messages versus how much came from no longer queuing behind a connection pool at capacity. A team seeing a result like this has evidence the combined migration worked for their call pattern and load — not evidence that swapping encoding format alone produces a 7x latency drop.

A separate independent test comparing the two protocols under moderate concurrency found a different pattern at the edges: minimum response times were similar (7ms for REST, 10ms for gRPC), but gRPC showed a higher maximum latency than REST in that specific low-load scenario. Protocol choice shifts the odds; it doesn't override load pattern, hardware, or whether the REST side was already running on HTTP/2.

One production migration: p99 latency before and after gRPCAn internal data pipeline's p99 latency dropped from 340ms on REST/JSON over HTTP/1.1 to 47ms after migrating to gRPC/Protobuf over HTTP/2 — a combined encoding-and-transport change, not an isolated encoding test.One Production Migration: p99 Latency Before and AfterCombined encoding + transport change — not an isolated encoding testREST/JSON over HTTP/1.1 (before)340ms p99gRPC/Protobuf over HTTP/2 (after)47ms p990100ms200ms300msSource: Boundev — gRPC vs REST: When to Use Which (2026)

None of this argues for ripping out REST everywhere. Public APIs consumed by browsers and third-party clients still favor REST's zero-setup, human-readable, universally-cached nature, and REST running over HTTP/2 closes much of the transport gap without changing the encoding at all. The pattern worth copying is narrower: keep REST at the edge where you don't control the client, reserve gRPC and Protobuf for calls between services you own, and before crediting Protobuf for a latency win, check whether the real driver was the transport upgrade that came bundled with it.

Comments (0)

Sort by:

No comments yet.

Be the first to share your perspective on this topic.