Blog

When JSON Is Too Slow: A Backend Engineer's Tour of gRPC

gRPC pairs a compact binary wire format with strict schemas and HTTP/2 streaming, trading JSON's flexibility for speed and contracts that catch breakage before deploy.

March 13, 20266 min readMuhammad Shehzaib
NETWORKINGGRPCPROTOBUFRPC

REST with JSON is comfortable, human readable, and debuggable with curl, which is exactly why teams keep reaching for it. But once you have services calling services thousands of times a second, the cost of parsing text, the absence of a real contract, and the lack of native streaming start to hurt. gRPC takes a different bet: define your service in a schema file, generate strongly typed clients and servers in any language, and send compact binary over HTTP/2 multiplexed connections. Coming from Express handlers and Mongoose models, the unfamiliar part is that the schema comes first and the code is generated from it, not the other way around. This article explains protocol buffers, the four call types, why HTTP/2 matters, how versioning works, and where gRPC fits versus the REST you already know. It is about internal service communication, not browser-facing APIs.

Schema First With Protobuf

A protocol buffers file, the proto, is the source of truth for a gRPC service. You declare messages as numbered fields and services as named methods that take one message and return another, then a compiler generates client and server code in Go, JavaScript, Python, and more. The field numbers, not the names, are what travels on the wire, which is the key to how protobuf stays compact and how it evolves safely. Coming from a Mongoose schema that lives in the same language as your code, the shift is that the contract is language-neutral and shared across every service that speaks it. This eliminates a whole category of integration bugs where the consumer and producer quietly disagreed about a field's shape, because both sides are generated from the same file and will not even compile if they drift apart.

The Binary Wire Format

JSON sends field names as text on every single message, so a field called createdAt costs nine bytes per occurrence before you even encode the value. Protobuf instead tags each field with its number and a wire type in a compact varint encoding, so that same field might cost two or three bytes total. Integers use variable-length encoding that makes small numbers tiny, and absent fields take zero bytes rather than appearing as null. The result is payloads often several times smaller than equivalent JSON, with parsing that is faster because there is no text to tokenize. The tradeoff is real: you cannot eyeball a protobuf message in your logs, and you need the schema to decode it at all. For internal high-volume traffic that is an easy trade, but it is exactly why you would not expose raw gRPC to a browser developer expecting readable responses.

Four Kinds of Calls

gRPC defines four method shapes, and choosing the right one is most of the design work. Unary is the familiar one request, one response, behaving like a REST call. Server streaming sends one request and gets back a stream of responses, ideal for pushing a feed of updates or paging through a large result without buffering it all. Client streaming sends a stream of requests and gets one response, suited to uploads or aggregating many events into a single summary. Bidirectional streaming opens both directions at once over the same connection, letting client and server send independently, which fits chat-like or interactive protocols. These streaming modes come for free from HTTP/2 and are awkward or impossible to model cleanly in plain REST. Picking unary everywhere is a common beginner mistake that throws away one of gRPC's biggest advantages over the JSON APIs you already write.

Why HTTP/2 Underneath

gRPC runs on HTTP/2, and that choice drives much of its behavior. HTTP/2 multiplexes many concurrent requests over a single TCP connection, so a hundred in-flight calls share one socket instead of each needing its own, eliminating the head-of-line blocking and connection churn that plague HTTP/1.1 at scale. It also supports full-duplex streaming, which is what makes gRPC's streaming call types possible, and it compresses headers so repeated metadata is cheap. The practical consequence for you is that gRPC clients hold long-lived connections and reuse them, so connection pooling works differently than the per-request model you may assume from HTTP/1.1 libraries. It also means intermediaries must understand HTTP/2 end to end; a proxy that downgrades to HTTP/1.1 will break gRPC, which is why proxying gRPC needs deliberate configuration rather than the defaults that work for ordinary REST.

Versioning Without Breaking

Protobuf was built for schemas that evolve across teams that deploy independently, and its rules are simple once you internalize them. Never reuse or change a field number, because the number is the identity on the wire; renaming a field is safe since names are not transmitted, but reusing a retired number silently corrupts data. Add new fields with new numbers and old clients ignore them, while new clients treat missing fields as defaults, giving you forward and backward compatibility for free. Mark removed fields as reserved so nobody accidentally reclaims their numbers later. This discipline is what lets a producer roll out a new field on Monday and consumers adopt it whenever they are ready, with no coordinated deploy. It is a sharp contrast to untyped JSON, where a renamed field can break a consumer at runtime with nothing catching it until production errors appear.

Errors and Deadlines

gRPC replaces HTTP status codes with its own set of status codes like NOT_FOUND, INVALID_ARGUMENT, and UNAVAILABLE, which map more precisely onto RPC semantics than the overloaded HTTP numbers you stretch to fit in REST. Each call can carry a deadline, an absolute time by which it must complete, and crucially that deadline propagates downstream: if service A calls B which calls C, the remaining time shrinks at each hop so the whole chain abandons work once the original caller has given up. This prevents the wasted effort where a service keeps computing a response nobody is waiting for anymore. Combine deadlines with retries carefully, because retrying a non-idempotent call after a deadline can duplicate work. Treat UNAVAILABLE as safe to retry with backoff and treat application errors as terminal, and you avoid amplifying a struggling service into a full outage.

Talking To Browsers

Browsers cannot speak raw gRPC because they do not expose the low-level HTTP/2 control that the protocol needs, so a web page cannot call a gRPC service directly. The bridge is gRPC-Web, a variant that works over what browsers can do, paired with a proxy layer that translates between gRPC-Web and standard gRPC behind the scenes. This is why a common architecture keeps gRPC strictly for service-to-service traffic inside your network and exposes a REST or GraphQL gateway to the public, with the gateway translating outward calls into internal gRPC. For your stack, that means Go services might talk gRPC among themselves while the Node or NestJS edge serves JSON to the React app. Reaching for gRPC at the browser boundary usually adds tooling pain without the payoff, so let the protocol live where its strengths actually apply.

When To Skip gRPC

gRPC is not a universal upgrade over REST, and choosing it reflexively costs you more than it returns in plenty of cases. If your API is public, consumed by third parties, or needs to be explorable with a browser and curl, JSON over REST wins on accessibility and tooling maturity. If traffic is modest and human debuggability matters more than microseconds, the binary format is a net negative. The build step that generates code from protos adds friction to your pipeline, and not every language or framework has equally polished support. gRPC earns its keep in high-volume internal communication, polyglot service meshes where strong contracts prevent drift, and anywhere streaming is a first-class need. Reach for it when you have many services chattering at scale; keep REST when humans are the primary consumers or when the operational simplicity of plain JSON is worth more than the performance you would gain.