gRPC uses Protocol Buffers (binary) over HTTP/2; REST uses JSON over HTTP/1.1 or HTTP/2
gRPC supports four streaming patterns natively; REST needs workarounds like WebSockets
gRPC is 2-5x faster for frequent small-payload calls; REST is baseline
gRPC requires code-generated clients; REST works with any HTTP client
Biggest mistake: using gRPC for public APIs without a REST gateway
Plain-English First
REST uses HTTP and JSON — the universal language of the web. Any browser, curl command, or HTTP client speaks it natively. gRPC uses Protocol Buffers and HTTP/2 — a binary format that's faster and more efficient but requires a code-generated client. REST is the front door everyone can use. gRPC is the dedicated freight entrance for services that need to move a lot of data fast.
I've built and maintained both REST and gRPC APIs in production. The decision is rarely about technical superiority — both work. It's about the consumers and their requirements. External API consumed by customers? REST. Internal microservice-to-microservice communication that processes 50,000 RPCs per second? gRPC. The mistake I see is teams defaulting to one or the other without asking 'who is consuming this and what do they need?'
In 2022, we migrated an internal order-processing service from REST to gRPC. The service handled inter-service communication between six microservices, averaging 30,000 requests per minute. After migration: 40% reduction in serialisation overhead, 60% reduction in request latency at P99. The consumer apps stayed on REST. The internal fabric went gRPC. Both in production, each doing what it's good at.
Protocol and Serialisation: Where the Performance Difference Comes From
REST typically sends JSON over HTTP/1.1. JSON is text — human readable, but verbose. Every field name is repeated as a string in every message. HTTP/1.1 opens a new TCP connection per request (or reuses one with keep-alive, but still has head-of-line blocking).
gRPC uses Protocol Buffers (protobuf) over HTTP/2. Protobuf is binary — fields are identified by integer tags, not string names. The same data that takes 200 bytes as JSON might take 40 bytes as protobuf. HTTP/2 multiplexes multiple requests over a single TCP connection and supports full-duplex streaming.
The performance difference is real but often overstated in benchmarks that don't reflect production conditions. The 3-5x latency improvement cited for gRPC over REST usually holds for high-frequency, small-payload inter-service calls. For large payloads or infrequent calls, the difference shrinks.
json serialisation overhead becomes a real cost above ~10k req/min.
protobuf encoding is not just smaller — it avoids the string parsing that spikes CPU.
observe the p99 latency shift when switching from Jackson to protobuf in your call chain.
Key Takeaway
protobuf beats json on wire size and parsing speed.
the difference matters most at scale.
never benchmark without realistic payload sizes and concurrency patterns.
Serialisation Choice
IfPayloads under 1KB, high frequency (>100 req/s)
→
UsegRPC/protobuf gives 3-5x latency improvement
IfPayloads over 100KB, low frequency
→
UseREST/JSON is fine — protobuf encoding cost offsets size advantage
REST: Why It Still Wins for Most APIs
REST's dominance for public and external APIs isn't about technical merit — it's about ubiquity. Every HTTP client in every language speaks REST. Browsers speak REST natively. curl speaks REST. Postman speaks REST. Your customers' mobile app developers know REST. Your partners' integration teams know REST.
gRPC requires a generated client from the .proto definition. Your API consumers need to run protoc, add gRPC dependencies, and handle the generated code. For internal services under your control, this is a minor inconvenience. For external API consumers who might be using PHP, Ruby, or a legacy Java stack, it's a barrier.
REST also wins on tooling maturity: API gateways, load balancers, proxies, observability tools, and browsers all understand HTTP/JSON natively. gRPC on HTTP/2 requires specific support at every layer.
A common production pattern: REST-facing API gateway that external consumers call, internally routing to gRPC microservices. The gateway translates HTTP/JSON to protobuf. External consumers get REST ergonomics. Internal services get gRPC performance. Tools like gRPC-Gateway or Envoy proxy make this straightforward. We run this exact architecture on a payments platform handling £2M daily volume.
Production Insight
forcing gRPC on external partners often results in abandonment or custom wrappers.
the cost of supporting protobuf clients for every consumer outweighs performance gains.
if you must expose gRPC externally, provide a REST proxy as a fallback.
Key Takeaway
REST wins on ecosystem penetration.
your api's success depends on consumers’ ability to consume it.
ubiquity trumps raw speed for public interfaces.
Streaming: The Capability REST Can't Match
gRPC's streaming support is its genuinely unique capability. REST over HTTP/1.1 is inherently request-response. Long polling, WebSockets, and Server-Sent Events are REST workarounds for streaming — they work but they're not native to the protocol.
gRPC has four communication patterns built into the protocol:
Unary: one request, one response. Same as REST.
Server streaming: one request, stream of responses. Useful for: real-time notifications, progress updates, large dataset pagination without polling.
Client streaming: stream of requests, one response. Useful for: bulk ingestion, file upload with progress tracking.
Bidirectional streaming: both sides stream simultaneously. Useful for: real-time chat, collaborative editing, live dashboards.
If your use case involves any of these patterns, gRPC's native streaming is significantly cleaner than working around REST's limitations.
TransactionStreamingService.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package io.thecodeforge.payment.grpc;
import io.grpc.stub.StreamObserver;
import java.util.List;
publicclassTransactionStreamingServiceImplextendsTransactionServiceGrpc.TransactionServiceImplBase {
privatefinalTransactionRepository transactionRepo;
publicTransactionStreamingServiceImpl(TransactionRepository repo) {
this.transactionRepo = repo;
}
// Server streaming: client sends one filter, server streams matching transactions// Useful for: exporting large datasets without pagination loops
@OverridepublicvoidstreamTransactions(
TransactionFilter filter,
StreamObserver<Transaction> responseObserver) {
// Fetch and stream — client receives each transaction as it's sent// No need to buffer the entire result set in memory
transactionRepo.findByCustomerIdAndDateRange(
filter.getCustomerId(),
filter.getFromTimestamp(),
filter.getToTimestamp()
).forEach(tx -> {
Transaction proto = Transaction.newBuilder()
.setTransactionId(tx.getId())
.setAmountPence(tx.getAmountPence())
.setCurrency(tx.getCurrency())
.setTimestamp(tx.getTimestamp())
.build();
responseObserver.onNext(proto); // Each transaction sent immediately
});
responseObserver.onCompleted();
}
// Client streaming: client sends batch of payments, server responds once
@OverridepublicStreamObserver<PaymentRequest> batchPayments(
StreamObserver<BatchResult> responseObserver) {
returnnewStreamObserver<>() {
int processed = 0;
int failed = 0;
@OverridepublicvoidonNext(PaymentRequest request) {
// Process each payment as it arrives from clienttry {
processPayment(request);
processed++;
} catch (Exception e) {
failed++;
}
}
@OverridepublicvoidonCompleted() {
// Client done sending — send summary response
responseObserver.onNext(BatchResult.newBuilder()
.setProcessed(processed)
.setFailed(failed)
.build());
responseObserver.onCompleted();
}
@OverridepublicvoidonError(Throwable t) {
// Handle client-side error
}
};
}
}
streaming without backpressure is a memory leak waiting to happen.
always test streaming under realistic concurrency — one slow client can crash the server.
monitor the number of open streams; set a maximum via an interceptor.
Key Takeaway
native streaming is gRPC's killer feature.
rest workarounds are brittle and add operational complexity.
if your data flows in one direction for >1 second, streaming is the right choice.
Error Handling and Status Codes: Differences That Matter in Production
REST uses HTTP status codes: 200 for success, 400 for bad request, 404 for not found, 500 for server error. These are well-understood but limited. Custom error messages go in the response body and are not standardised across implementations.
gRPC defines a set of status codes in protobuf (like INVALID_ARGUMENT, NOT_FOUND, INTERNAL, UNAVAILABLE) that are standard across all implementations. Every gRPC response includes a status code and an optional error message. Additionally, gRPC supports rich error models through the google.rpc.Status proto, including error details with structured information.
The critical difference is that gRPC's error model is consistent across all client languages — the same status code means the same thing in Java, Go, or Python. REST depends entirely on documentation and convention.
GrpcErrorHandling.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package io.thecodeforge.payment.grpc;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.protobuf.StatusProto;
import com.google.rpc.Code;
import com.google.rpc.RetryInfo;
import com.google.protobuf.Duration;
publicclassGrpcErrorHandling {
// ❌ Wrong: throwing generic Status.INTERNALpublicvoidprocessPaymentBad(String id) {
if (id == null) {
throwStatus.INTERNAL
.withDescription("null customer id")
.asRuntimeException();
}
// This gives no indication to client that it's a client error
}
// ✅ Correct: using the specific status code with rich error detailspublicvoidprocessPaymentGood(String id) {
if (id == null) {
// Build a structured error with retry information
com.google.rpc.Status status = com.google.rpc.Status.newBuilder()
.setCode(Code.INVALID_ARGUMENT.getNumber())
.setMessage("customer id must not be null")
.addDetails(
RetryInfo.newBuilder()
.setRetryDelay(Duration.newBuilder()
.setSeconds(5)
.build())
.build()
)
.build();
throwStatusProto.toStatusRuntimeException(status);
}
}
// Client side: different codes lead to different recoverypublicstaticvoidmain(String[] args) {
try {
// call grpc service
} catch (StatusRuntimeException e) {
switch (e.getStatus().getCode()) {
case INVALID_ARGUMENT:
// Log and stop retrying — bad requestbreak;
caseUNAVAILABLE:
// Retry with backoff — transientbreak;
case DEADLINE_EXCEEDED:
// Increase timeout or check downstreambreak;
}
}
}
}
Output
// Client output for INVALID_ARGUMENT error:
// StatusRuntimeException: INVALID_ARGUMENT: customer id must not be null
// Retry info: retry after 5 seconds
Mistake-Proof Your gRPC Status Codes
INVALID_ARGUMENT: client sent bad data — don't retry.
UNAVAILABLE: server can't serve — retry with backoff.
DEADLINE_EXCEEDED: slow processing — reduce load or increase timeout.
INTERNAL: unexpected server error — log, alert, fix code.
Teach your team this mapping to avoid debugging incidents.
Production Insight
using INTERNAL for everything hides the root cause from clients.
without structured errors, clients cannot differentiate between retryable and fatal.
always map domain errors to the most specific gRPC status code.
Key Takeaway
use specific status codes to let clients react intelligently.
rest forces clients to guess from ad-hoc error bodies.
gRPC's standard codes make distributed error handling a solvable problem.
Tooling and Ecosystem: Where REST Still Leads
REST has decades of tooling maturity. Postman, Insomnia, curl, and browser dev tools all understand HTTP/JSON natively. API gateways like Kong, AWS API Gateway, and Nginx handle REST without configuration. Monitoring tools parse HTTP methods, status codes, and response times out of the box. Load balancers distribute REST requests without special setup.
gRPC tooling has improved but still lags. grpcurl and BloomRPC exist, but they're not as popular as Postman. API gateways need explicit HTTP/2 support and gRPC-web translation for browsers. Monitoring gRPC calls requires special instrumentation: you need the service name, method, and status code — not just HTTP metadata. Some tools now support gRPC natively (like gRPC reflection for service discovery), but the gap is real.
If your team is small or you need to onboard junior engineers quickly, the REST toolchain gives everyone a head start. gRPC requires learning protoc, generated stubs, and a new debugging workflow.
debug-tools.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
# Debugging tools comparison
# REST: universally available
curl https://api.thecodeforge.io/v1/payments/pay-123 -H 'Accept: application/json'
# gRPC: requires grpcurl and reflection or proto descriptors
grpcurl -plaintext payment-service.internal:50051 list
grpcurl -plaintext payment-service.internal:50051 io.thecodeforge.payment.grpc.PaymentService/ProcessPayment
# REST: easy to inspect in browser devtools
echo "Open Chrome DevTools → Network tab → any request"
# gRPC: need proxy or packet capture
echo "Use tcpdump or grpcurl with -d '{}' to send test calls"
# REST: metrics in every monitoring tool
curl http://prometheus:9090/api/v1/query?query=http_requests_total{handler="/api/v1/payments"}
# gRPC: metrics require custom interceptors (micrometer or prometheus client)
echo "Add gRPC server interceptor: new MetricsServerInterceptor(...)"
debugging a broken gRPC call without grpcurl is like debugging curl without httpie.
always deploy gRPC reflection in non-production environments — it saves hours of guessing.
invest in REST tooling for external debugging; invest in gRPC interceptors for internal observability.
Key Takeaway
rest eco-system is 10 years ahead on debugging and monitoring.
grpc is catching up but requires intentional tooling investment.
choose based on your current team's proficiency and support infrastructure.
● Production incidentPOST-MORTEMseverity: high
gRPC Service Outage Due to Unbounded Stream Buffers
Symptom
gRPC service running on Kubernetes with 4GB heap crashes every 6 hours with OutOfMemoryError. Pods restart, but during restart the downstream services get connection errors.
Assumption
The team assumed gRPC's built-in flow control would prevent memory issues. They thought HTTP/2's stream multiplexing would handle backpressure automatically.
Root cause
The server-side implementation queued all outgoing messages in an in-memory list before calling onNext(). Under high load, the client consumed messages slower than the server produced them. gRPC's flow control paused the stream, but the server continued buffering because the StreamObserver didn't have a feedback mechanism. Heap grew until OOM.
Fix
Replaced the in-memory buffer with a bounded blocking queue. Set a maximum buffer size per stream. Added a direct flow-control check: if queue is full, reject the request with status RESOURCE_EXHAUSTED. Also enabled per-stream memory limits in the gRPC interceptor.
Key lesson
gRPC flow control stops transmission, but it does not stop you from buffering on the server side. Always pair streaming with bounded buffers.
Monitor JVM heap per stream during development — a single misbehaving client can eat all memory.
Use gRPC's FlowControlHandler or custom interceptors to enforce per-stream backpressure.
Production debug guideSymptom→Action guide for common gRPC and REST production failures4 entries
Symptom · 01
gRPC call fails with UNAVAILABLE status
→
Fix
Check if server is reachable: grpcurl -plaintext <host>:<port> list. If unreachable, verify DNS and firewall. If reachable, check the load balancer's HTTP/2 support — some LBs downgrade to HTTP/1.1.
Symptom · 02
REST endpoint returns 502 Bad Gateway
→
Fix
Check upstream service health endpoint. If healthy, look at timeout configuration in the API gateway. Many 502s come from gateway timeout settings mismatched with upstream processing time.
Symptom · 03
gRPC streaming call slowly consumes more memory
→
Fix
Attach heap dump and look for large byte arrays held by netty or gRPC OutboundMessageQueue. Use jmap -heap:format=b and analyze with Eclipse MAT. Look for 'io.grpc.internal.MessageDeframer' instances.
Symptom · 04
REST API response times increase linearly with concurrent requests
→
Fix
Check connection pool exhaustion. Use netstat to see TIME_WAIT connections. Increase max connections or switch to HTTP/2 to multiplex.
★ Quick Debug Cheat Sheet: gRPC & RESTCommands and immediate actions for the most common production issues.
gRPC call returns DEADLINE_EXCEEDED−
Immediate action
Check downstream service latency. Then verify deadline propagation across service chain.
Commands
grpcurl -plaintext -d '{}' <host>:<port> <service>/<method> | head -c 200
Rollback the API to previous version and add proper version negotiation.
gRPC vs REST at a Glance
Aspect
REST
gRPC
Protocol
HTTP/1.1 or HTTP/2
HTTP/2 only
Serialisation
JSON (text)
Protocol Buffers (binary)
Contract
OpenAPI/Swagger (optional)
.proto file (required)
Code generation
Optional
Required (client + server stubs)
Browser support
Native
Requires grpc-web proxy
Streaming
Workarounds (SSE, WebSocket)
Native (4 patterns)
Performance
Baseline
~2-5x faster for frequent calls
Error handling
HTTP status codes + custom body
Standardised status codes + rich error model
Tooling
Universal (Postman, curl, etc.)
Specialised (BloomRPC, grpcurl)
Best for
Public APIs, external consumers
Internal microservices, high-frequency calls
Key takeaways
1
REST over HTTP/JSON for external APIs, public-facing endpoints, and any consumer you don't control
ubiquity and tooling win.
2
gRPC over protobuf/HTTP/2 for internal microservice communication, high-frequency calls, and when streaming is required.
3
gRPC is 2-5x faster for frequent small-payload calls. For large payloads or infrequent calls, the difference is smaller.
4
The .proto file is the source of truth for a gRPC API
it enforces a strict contract, enables code generation in any language, and prevents the schema drift common in JSON APIs.
5
You don't have to choose
REST gateway for external traffic routing to internal gRPC services is a standard production pattern.
Common mistakes to avoid
4 patterns
×
Using gRPC for a public API without a REST/JSON facade
Symptom
External developers cannot easily consume gRPC without generated clients. They complain about setup complexity. Some abandon the API entirely.
Fix
Provide a REST gateway that translates HTTP/JSON to protobuf. Deploy gRPC-Gateway or Envoy in front of the gRPC service. Offer both gRPC and REST endpoints documented side by side.
×
Choosing REST for high-frequency internal service calls because 'it's simpler'
Symptom
At 10,000+ RPCs per minute, CPU usage spikes due to JSON serialisation. P99 latency grows due to HTTP/1.1 connection overhead and head-of-line blocking.
Fix
Migrate to gRPC for internal services that exchange small payloads frequently. Use protobuf for the serialisation improvement and HTTP/2 for multiplexing. The migration effort pays off within weeks at that scale.
×
Not defining .proto contracts upfront for gRPC
Symptom
After a few field modifications, the .proto file no longer matches the actual data structure. Field numbers are reused or changed, causing silent data corruption. Clients break at runtime without obvious errors.
Fix
Treat the .proto file as the single source of truth. Never change field numbers or types after the contract is in use. Use reserved fields for retired fields. Enforce schema validation in CI to detect drift.
A chain of microservices (A→B→C) where only A sets a deadline. B and C have no deadline context, so B waits indefinitely for C. A eventually times out, but B is stuck handling a request that A already gave up on. Cascading timeout storms follow.
Fix
Always propagate the gRPC deadline via the Context. Use Context.current().withDeadlineAfter(...) and pass it to downstream calls. Set a maximum deadline at the edge service. Never create a new Context without inheriting the deadline.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01JUNIOR
What are the main differences between gRPC and REST? When would you choo...
Q02SENIOR
Explain how Protocol Buffers differ from JSON and what performance impli...
Q03SENIOR
What are the four communication patterns in gRPC and when would you use ...
Q04SENIOR
You're designing a microservices architecture with 8 internal services a...
Q05SENIOR
What is gRPC-Gateway and why would you use it?
Q01 of 05JUNIOR
What are the main differences between gRPC and REST? When would you choose each?
ANSWER
gRPC uses Protocol Buffers over HTTP/2, supports four streaming patterns, requires code-generated clients, and has standardised status codes. REST uses JSON over HTTP/1.1 (or HTTP/2), is request-response only, works with any HTTP client, and uses HTTP status codes.
Choose gRPC for internal microservice communication, high-throughput scenarios (>10k req/min), and when you need native streaming. Choose REST for public APIs, any consumer you don't control, browser-facing services, or when onboarding simplicity matters more than raw performance.
Q02 of 05SENIOR
Explain how Protocol Buffers differ from JSON and what performance implications that has.
ANSWER
Protocol Buffers are binary — fields are identified by integer tags, not string names. The same data as JSON takes roughly one-third the bytes. Parsing protobuf is a direct binary read, while JSON requires string tokenization and object construction. For frequent small payloads (<1KB), protobuf is 3-5x faster. For infrequent or large payloads, the benefit narrows because parsing time is dominated by I/O. The catch: protobuf requires a schema and code generation, so it adds a dependency and build step.
Q03 of 05SENIOR
What are the four communication patterns in gRPC and when would you use each?
ANSWER
1) Unary: one request, one response — replaces REST endpoints.
2) Server streaming: one request, streamed responses — for real-time notifications, progress updates, pagination without polling.
3) Client streaming: streamed requests, one response — for batch uploads, sensor data ingestion, where client sends multiple items and server responds once.
4) Bidirectional streaming: both sides stream independently — for chat, collaborative editing, real-time dashboards, or any scenario where both sides send data simultaneously.
Use server or bidirectional streaming when latency matters and you want to avoid polling. Use client streaming when you want to process data as it arrives instead of waiting for a complete batch.
Q04 of 05SENIOR
You're designing a microservices architecture with 8 internal services and a public API. How would you decide which services use gRPC vs REST?
ANSWER
Start by identifying service boundaries: internal services that communicate frequently (high request rate, small payloads) are candidates for gRPC. The public API layer should expose REST endpoints for external consumers. Use a gateway (e.g., gRPC-Gateway) to translate REST to internal gRPC calls. Services that stream data or require real-time updates (like a notification service) benefit from gRPC streaming. Services that rarely communicate or have large payloads (like a reporting service) can stay on REST. The decision should be based on request volume, latency requirements, and consumer type — not a blanket choice.
Q05 of 05SENIOR
What is gRPC-Gateway and why would you use it?
ANSWER
gRPC-Gateway is a tool that generates a reverse proxy server from a .proto file. It exposes a RESTful JSON API that translates HTTP requests to gRPC calls. You'd use it when you want to offer both gRPC and REST interfaces to the same backend without writing separate controllers. It's especially useful when you have internal services using gRPC but external clients require REST. The alternative is manual implementation with something like Envoy's gRPC-JSON transcoder.
01
What are the main differences between gRPC and REST? When would you choose each?
JUNIOR
02
Explain how Protocol Buffers differ from JSON and what performance implications that has.
SENIOR
03
What are the four communication patterns in gRPC and when would you use each?
SENIOR
04
You're designing a microservices architecture with 8 internal services and a public API. How would you decide which services use gRPC vs REST?
SENIOR
05
What is gRPC-Gateway and why would you use it?
SENIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
When should I use gRPC instead of REST?
Use gRPC for internal microservice-to-microservice communication where performance matters, when you need native streaming (server push, client streaming, bidirectional), or when strong schema contracts and code generation are important. Use REST for public APIs, external consumers, browser-facing services, or any situation where you can't control client code generation.
Was this helpful?
02
Is gRPC faster than REST?
Yes, typically 2-5x faster for frequent small-payload calls. The advantages come from Protocol Buffers (binary serialisation, smaller payloads than JSON) and HTTP/2 (multiplexing, header compression, no head-of-line blocking). For infrequent calls or large payloads, the performance gap narrows. The difference matters most at high request volumes — 10,000+ RPCs per minute.
Was this helpful?
03
Can gRPC and REST coexist in the same system?
Yes, and this is a common production pattern. A REST/JSON API gateway handles external traffic and translates to internal gRPC calls. External consumers get REST ergonomics and universal tooling. Internal services get gRPC performance and streaming. gRPC-Gateway and Envoy proxy both support this pattern out of the box.
Was this helpful?
04
Does gRPC work in browsers?
Not natively. Browsers cannot make HTTP/2 requests with the required trailers that gRPC uses. The solution is grpc-web, a modified protocol with a JavaScript client library, combined with an Envoy or Nginx proxy that translates grpc-web to grpc on the server side. For browser-facing APIs, REST remains the simpler choice.
Was this helpful?
05
What's the biggest pitfall when implementing gRPC streaming in production?
The biggest pitfall is forgetting backpressure. gRPC's flow control pauses transmission, but it doesn't stop the server from buffering messages in memory. If the client is slow, the server's outbound buffer grows until OOM. Always use bounded queues and set per-stream memory limits. Monitor heap per stream in development. Also be wary of deadlock: if two services use bidirectional streaming and each blocks on the other's response, you get a deadlock. Design for non-blocking processing.