Karya Semi
HomeBlogSearchCategoriesAboutContact
Karya Semi

Less noise. More notes.

HomeBlogAboutContactPrivacy PolicyDisclaimer

© 2026 Karya Semi. All rights reserved.

XGitHubLinkedIn
  1. Home
  2. /Categories
  3. /Web Development

Modern Microservices Communication: gRPC vs REST in Distributed Systems

Analyze grpc vs rest microservices to build faster APIs. Discover how serialization, latency, and protocol buffers impact your distributed system architecture.

Dian Rijal Asyrof/August 15, 2026/9 min read
Illustration for Modern Microservices Communication: gRPC vs REST in Distributed Systems

An article discussing the differences between gRPC and REST, their performance characteristics, and when to use each.


We build microservices because we want to split up big, messy codebases. We want teams to work on their own schedules, deploy when they want, and write in whatever language makes sense for the job. But when we split a monolith into ten different services, we trade in-memory function calls for network hops. Suddenly, the network is our bottleneck.

For years, the default answer to this network problem was simple: use REST. You write some JSON, expose an HTTP endpoint, and let the other services call it. It is simple to build and debug. Plus, everyone knows how to do it.

But as systems grow, that simplicity starts to cost you. JSON parsing eats up CPU cycles. HTTP/1.1 connections clog up. Latency creeps up, and suddenly your distributed system feels sluggish. That is when people start looking at gRPC.

REST is an architectural style rather than a strict protocol. Most of the time, when we say REST, we mean JSON payloads sent over HTTP/1.1.

HTTP/1.1 is old. It was designed for web browsers loading documents, not for microservices talking to each other thousands of times a second. Under HTTP/1.1, a client opens a TCP connection, sends a request, and waits for the response. If you want to send another request, you either have to wait for the first one to finish or open a new TCP connection.

Opening TCP connections is expensive. It requires a handshake, which slows down your network and consumes system resources. To get around this, we use connection pools. We keep a handful of connections open and reuse them. But even with pooling, you hit a limit. If all connections in the pool are busy waiting for slow database queries, your client blocks. This is head-of-line blocking at the application level.

In HTTP/1.1, browsers and clients limit the number of parallel connections to a single domain. Usually, that limit is six. If your page needs to fetch twenty different resources, it can only fetch six at a time. The rest wait in line. In a microservice mesh, if service A needs to make multiple calls to service B, it faces the same bottleneck. You can increase the connection pool size, but each connection is a file descriptor, memory, and CPU overhead.

Then there is JSON. JSON is text. It is easy for humans to read, which is great for debugging. But computers do not read text naturally. They have to parse it, convert strings to numbers, allocate memory for keys, and serialize it back to text on the other end. When you are doing this millions of times a minute across dozens of services, a significant amount of your CPU time goes entirely to parsing JSON.

gRPC is a framework built by Google to solve these exact bottlenecks. It does two things differently: it uses HTTP/2 as its transport layer, and it uses Protocol Buffers (Protobuf) for serialization.

HTTP/2 changes how data moves over the wire. Instead of opening multiple TCP connections to handle concurrent requests, HTTP/2 uses a single TCP connection and multiplexes requests over it. It splits messages into binary frames and interleaves them. A client can send fifty requests at the same instant over one connection, and the server can send responses back as they finish, in any order. No waiting for the connection to clear. No head-of-line blocking on the network layer.

HTTP/2 also compresses headers. In HTTP/1.1, every request carries a bunch of plaintext headers, often repeating the exact same user-agent, content-type, and authorization headers. HTTP/2 uses HPACK compression, which keeps a table of headers on both sides and only sends the differences. This cuts down payload sizes.

Then we have Protocol Buffers. Instead of sending human-readable text, gRPC sends binary data. You define your data structure in a .proto file. A compiler takes that file and generates code for your programming language of choice. This generated code serializes your objects directly into a compact binary format.

Because it is binary, there are no key names sent over the wire. In JSON, if you have a field called user_id, that string "user_id" is sent in every single message. In Protobuf, that field is represented by a small integer tag. The payload is tiny, and parsing is fast because the computer does not have to parse strings. It just reads bytes.

Let's look at what this actually means for CPU and memory.

When you serialize an object to JSON, the library has to inspect the object, find the fields, convert numbers to string representations, and build a text payload. With Protobuf, the generated code knows the exact memory layout of the message. It writes the bytes directly to a buffer.

In high-throughput systems, this difference is massive. Benchmarks show that Protobuf serialization can be up to ten times faster than JSON serialization, depending on the language and the library you use. The payload size is often less than half of the equivalent JSON.

If your service handles 10,000 requests per second, switching from JSON to Protobuf can drop your CPU usage by 30% or more. That translates directly to lower cloud bills and faster response times.

But performance is not just about raw speed. It is also about network utilization. Because HTTP/2 multiplexes requests, you need far fewer TCP connections. Your load balancers do not have to manage tens of thousands of open sockets. Your firewalls have less state to track. The entire network path becomes quieter.

REST is fundamentally a request-response model. You send a request, you get a response. If you want real-time updates, you have to use workarounds like long-polling, Server-Sent Events (SSE), or switch to WebSockets. None of these are native to the REST design itself. They are separate protocols tacked on.

gRPC has streaming built directly into the core protocol. It supports four communication patterns.

First, unary RPCs. This is the classic request-response. The client sends a request and gets a response.

Second, server streaming. The client sends a request and gets a stream of responses. The server can keep sending messages as they become available. This is useful for things like tailing logs or sending real-time data feeds.

Third, client streaming. The client sends a stream of messages, and the server responds with a single message once the stream is complete. Think of uploading a large file in chunks.

Fourth, bidirectional streaming. Both client and server send a stream of messages simultaneously. The two streams operate independently, meaning the server can respond to each message as it arrives, or wait for all of them, or write some messages back even before the client finishes sending. This is powerful for chat applications or real-time gaming backends.

To understand why Protobuf is so small, we have to look at how it encodes data.

Consider a simple JSON object:

{"id": 123, "name": "Alice"}

This JSON string takes up 28 bytes of space. If you send this over the network, you are sending the characters {, ", i, d, ", :, , and so on. The actual data you care about is just the number 123 and the string "Alice". The rest is formatting.

In Protobuf, the schema defines id as field 1 and name as field 2. When serialized, the binary payload looks like a sequence of key-value pairs, but the keys are just numbers. The key contains the field number and the wire type (which tells the parser how to read the following bytes).

The number 123 is stored using a variable-length quantity (varint), which takes up only one byte. The string "Alice" is stored as the field number, followed by the length of the string (5) and the raw ASCII bytes.

The entire Protobuf payload for this object is less than 10 bytes. You cut your bandwidth usage by more than half for a simple object. For large, complex objects with nested arrays and long field names, the savings are even larger.

One of the biggest issues with REST is that it does not enforce a contract. You can write an endpoint, document it on a wiki or in a Swagger file, and hope the client team reads it. If you change a field name or delete a parameter, you break the client. You only find out when the integration tests fail, or worse, when errors spike in production.

gRPC forces you to write a schema first. The .proto file is the source of truth.

syntax = "proto3";
 
package users;
 
service UserService {
  rpc GetUser (UserRequest) returns (UserResponse);
}
 
message UserRequest {
  string user_id = 1;
}
 
message UserResponse {
  string id = 1;
  string email = 2;
  int64 created_at = 3;
}

You run this through the protoc compiler, and it generates the client SDK and the server stubs. If you are writing your gateway in Go and your user service in Rust, the compiler handles both. The types are strict. If you try to send a string where an integer is expected, the code will not compile.

This design prevents a massive class of bugs. You do not have to write manual validation code to check if a field exists or if it is the right type. The generated code handles it.

Version control is also built into the design. Protobuf uses field numbers (like the 1, 2, and 3 in the example above) to identify fields. If you want to add a new field, you just give it a new number. Old clients will ignore the new field; new clients will read it. As long as you do not change the field numbers of existing fields, you can evolve your API without breaking older versions.

When you adopt gRPC, your development workflow changes. In a REST environment, a developer might write a controller, run the app, and test it with curl. If they change a field, they might update a wiki page.

With gRPC, you start by modifying the proto file. You commit the proto file to a repository.

There are two main ways teams manage proto files: a monorepo or separate repositories.

In a monorepo, all proto files live in a single folder. A CI/CD pipeline runs every time a proto file changes. It compiles the files into client and server libraries for Go, Node.js, Python, or whatever languages your team uses, and publishes them as internal packages (like npm packages or Go modules). When a developer wants to use the new API, they just update their package dependencies.

In a multi-repo setup, you might keep the proto files inside the service repository that implements them. Other services then import the proto files directly or use a tool to fetch them. This can get messy quickly, as you have to manage cross-repository dependencies and versioning.

Either way, the code generation step is mandatory. You cannot write code without compiling the protos first. This introduces a build step that REST developers are not used to. If you make a mistake in your proto syntax, your build fails. It adds friction to the local development loop, but it guarantees that the code running in production matches the API definition.

Despite the speed and bandwidth benefits, teams still choose REST for several reasons.

First, it is hard to use from a web browser. Browsers do not expose the low-level control over HTTP/2 frame headers that gRPC needs. You cannot write a standard JavaScript fetch call to a gRPC backend. To connect a frontend app to a gRPC service, you have to use gRPC-Web, which requires running a proxy like Envoy to translate browser requests into standard gRPC. It adds moving parts to your infrastructure.

Second, debugging is harder. With REST, if you want to see what an endpoint returns, you use curl or open a browser tab. You can inspect the JSON payload in your browser's network tab. With gRPC, the payload is binary. If you try to curl a gRPC endpoint, you get garbage characters. You need tools like grpcurl or Postman, and you need access to the proto files to decode the messages. It makes ad-hoc testing and debugging more difficult.

Third, load balancing gets complicated. Because HTTP/2 keeps a single TCP connection open for a long time and multiplexes requests over it, traditional Layer 4 load balancers (which balance connections) do not work well. If a client opens a connection to one instance of your service, all subsequent requests will go to that same instance, even if you scale up and add five more instances. You need Layer 7 load balancers (like Envoy or Linkerd) that understand HTTP/2 frames and can balance individual requests over the open connections.

The choice between gRPC and REST is not about finding the better technology. It is about matching the technology to the boundary.

For internal microservices, where services talk to other services inside your network, gRPC is usually the right choice. The performance gains and strict types outweigh the debugging friction. You control the entire environment, so setting up Layer 7 load balancing and managing proto files is manageable.

For public APIs, external integrations, or frontend-to-backend communication, REST is still the standard. Everyone knows how to consume a REST API. You do not want to force your external developers to install gRPC tools or compile proto files just to use your service. The simplicity of JSON and HTTP/1.1 is worth the performance trade-off.

You do not have to choose just one, either. Many architectures use a hybrid approach. They use a REST gateway (or GraphQL) at the edge of the network to handle incoming traffic from browsers and mobile apps, and then use gRPC for all the communication between internal services behind the gateway. This gives you the best of both: a simple, accessible public interface and a fast, type-safe internal network.

DR

Dian Rijal Asyrof

Writes about useful AI tools, programming practice, and the craft of building reliable software.

Previous articlePractical Private AI: Homomorphic Encryption and Fully Encrypted Inference

See also

Illustration for Building Responsive UI Components with CSS Container Queries
Web Development/Aug 14, 2026

Building Responsive UI Components with CSS Container Queries

Discover how css container queries responsive design lets you build modular components that adapt to their parent element size rather than the viewport.

8 min read
CSSWeb Development
Illustration for Offloading Heavy Computations with Web Workers in Modern JavaScript
Web Development/Aug 13, 2026

Offloading Heavy Computations with Web Workers in Modern JavaScript

Keep your UI responsive by running CPU-heavy tasks in the background. Learn how to implement javascript web workers to boost performance and prevent page freezing.

7 min read
Web DevelopmentJavaScript
Illustration for The Download-as-ZIP Button That Crashes Your Users' Browsers
Web Development/Aug 11, 2026

The Download-as-ZIP Button That Crashes Your Users' Browsers

Learn how to implement a robust React download multiple files zip browser feature that handles massive archives with progress tracking and cancellation.

6 min read
ReactJavaScript