Once a system splits into services, every internal call becomes a network call, and the protocol choice compounds across hundreds of services. Interviewers use "REST or gRPC internally?" to check whether you understand the latency, tooling, and typing trade-offs — not just that gRPC is "faster."
REST is a way for two programs to talk over the internet using plain, readable messages — the same basic technology your browser uses to load a web page. A request asks for something like "get user 123," and the response comes back as ordinary, human-readable text (JSON) that's easy for any programmer, tool, or browser to read and debug.
gRPC is a faster, more compact way for two programs to talk, designed for services that call each other constantly behind the scenes rather than for a human eyeballing the traffic. Both sides agree on a strict contract in advance for exactly what a request and response look like, then send that data packed down into dense binary instead of readable text — smaller and quicker to process, at the cost of no longer being human-readable.
REST is like ordering at a restaurant counter in plain spoken language — "I'd like a medium coffee, oat milk" — anyone working the counter can understand it immediately, even a new hire, but it takes longer to say than it needs to. gRPC is like a kitchen's internal ticket system, where the same order becomes a short pre-agreed code that only kitchen staff who know the code can read — faster to process, but useless to someone who wasn't trained on that code.
A REST call like GET /users/123 over HTTP/1.1 sends human-readable text — headers, then a JSON body — and, in the common case, opens a new TCP connection (or reuses one via keep-alive) per logical request stream. A gRPC call is a typed method invocation, GetUser(userId), serialized to compact binary Protocol Buffers and sent as an HTTP/2 stream, where many concurrent calls share one underlying TCP connection.
REST/JSON over HTTP/1.1
GET /users/123 HTTP/1.1
Host: api.example.com
Authorization: Bearer ...
Response body (≈180 bytes for a small object):
{"id":123,"name":"Ada Lovelace","email":"[email protected]","active":true}
gRPC/Protobuf over HTTP/2
.proto contract:
service UserService {
rpc GetUser(GetUserRequest) returns (User);
}
message User { int64 id = 1; string name = 2; string email = 3; bool active = 4; }
Wire bytes (≈40-60 bytes for the same object):
field 1 (id): varint 123
field 2 (name): len-prefixed "Ada Lovelace"
field 3 (email): len-prefixed "[email protected]"
field 4 (active): varint 1
— field names never travel on the wire, only numbered tags; the schema
(the .proto file) supplies the names on both ends.
That difference — field names in every JSON response vs. numbered tags resolved against a shared, compiled schema — is most of where gRPC's 4-10x size reduction comes from, on top of HTTP/2's binary framing and multiplexing (many logical streams over one TCP connection, no head-of-line blocking at the application layer the way multiple HTTP/1.1 connections can have at the TCP layer).
REST is fundamentally one shape — request in, response out. gRPC, because it's built on HTTP/2 streams instead of one-shot request/response, supports four:
rpc GetUser(GetUserRequest) returns (User) — the REST-equivalent single request, single response.rpc ListOrders(ListOrdersRequest) returns (stream Order) — one request, a stream of responses (e.g. paginating a large result set without repeated round trips, or pushing live price updates).rpc UploadTelemetry(stream Metric) returns (UploadSummary) — a stream of requests, one final response (e.g. a client batching metrics/log lines and sending them continuously, with one ack at the end).rpc Chat(stream Message) returns (stream Message) — both sides stream independently over the same long-lived connection (e.g. a live collaborative editing session or a chat backend).This is the concrete reason "gRPC supports streaming" matters beyond a buzzword: it maps directly onto real use cases — service meshes streaming config updates, telemetry pipelines batching metrics, or any workload where request/response is too coarse-grained.
.proto file — services, methods, and message schemas — and generate client/server stubs for each language.Compare the two side by side against a real service. For REST, plain curl is enough:
# REST — inspect headers and body size directly
curl -v https://api.example.com/users/123
# gRPC — first, ask the server what services/methods it exposes
# (requires the server to support gRPC reflection)
grpcurl -plaintext localhost:50051 list
# Describe a specific method's request/response schema
grpcurl -plaintext localhost:50051 describe user.UserService.GetUser
# Actually invoke it, passing the request as JSON (grpcurl handles
# the JSON -> protobuf translation for you)
grpcurl -plaintext -d '{"userId": 123}' \
localhost:50051 user.UserService.GetUser
Compare the response times and payload sizes reported by each — on a real internal call this is where you'll see gRPC's binary framing and HTTP/2 multiplexing pay off, especially under concurrent load.
Q: Should every microservice use gRPC?
A: No. Choose based on who calls the API: public clients and browsers need REST/JSON's ubiquity, cacheability, and debuggability with tools everyone already has (curl, browser devtools). Internal service-to-service calls where you control both ends benefit from gRPC's typed contracts and performance. Also weigh ecosystem interoperability (does a partner's system only speak REST?), whether streaming actually matters for this call, latency sensitivity, and the operational cost — gRPC needs more tooling for browser clients (grpc-web plus a proxy) and for load balancers that weren't built with HTTP/2 streams in mind.
Q: Why is gRPC faster than REST/JSON in practice?
A: Two independent factors compound: the wire format (binary protobuf with numbered fields vs. text JSON with field names repeated every time) is smaller and faster to (de)serialize, and the transport (HTTP/2 multiplexed streams on one connection vs. HTTP/1.1's per-connection request queuing) removes a source of head-of-line blocking. Neither alone explains all of the gap — it's both together.
Q: How do you handle breaking changes to a gRPC API without downtime?
A: Prefer additive, backward-compatible changes — add new fields with new field numbers, never reuse or renumber a retired field, and treat every field as optional so old and new clients can both parse messages from the other version. For a genuine breaking change, version the service itself (e.g. a new UserServiceV2) and run both during a migration window, the same way you'd version a REST endpoint.
Q: Can a browser call a gRPC service directly?
A: Not standard gRPC — browsers can't set the raw HTTP/2 trailers gRPC needs. You need grpc-web, a JS client library that speaks a modified protocol, plus a proxy (e.g. Envoy) in front of your gRPC servers that translates grpc-web calls into real gRPC. This extra hop is exactly why public-facing APIs usually stay REST (or GraphQL) and gRPC stays behind the edge.
In a prompt like "design the backend for a food-delivery platform," the order service, the driver-matching service, the pricing service, and the restaurant service all talk to each other internally, at high volume, under latency pressure (a driver-matching decision has to happen in milliseconds). That's the textbook case for gRPC between services — typed contracts prevent an order-service deploy from silently breaking pricing-service's parsing, and the binary framing keeps p99 latency low under heavy fan-out. The public mobile app and web client, meanwhile, talk to an API gateway over REST (or GraphQL) — the gateway is where gRPC-internal, REST-external gets stitched together, and calling that out explicitly is what separates a strong answer from one that just says "gRPC everywhere."
gRPC's typed contracts catch integration bugs at compile time instead of runtime, and its binary framing plus HTTP/2 multiplexing meaningfully cuts p99 latency at high internal call volume — this is why most large-scale backends (and many model-serving stacks) use gRPC between services. REST remains better for anything public-facing: it's cacheable by generic HTTP infrastructure, debuggable with curl, and doesn't require clients to have your .proto files. A common interview trap is treating this as purely binary — real systems typically run gRPC internally and expose a REST (or GraphQL) gateway at the edge that translates for external clients.