Skip to main content

RPC Styles: gRPC, JSON-RPC, and When to Prefer Them

What This Concept Is

Remote Procedure Call (RPC) styles expose APIs as named procedures with typed inputs and outputs, not as resources with verbs. The two common ones today:

  • gRPC: contract-first via Protocol Buffers (.proto files), HTTP/2 transport, binary wire format, first-class bidirectional streaming, code generation for many languages.
  • JSON-RPC 2.0: minimal spec, JSON over any transport (usually HTTP), method name in payload, batch support.

RPC and REST are not opposites - REST is a subset of styles that happens to use URLs as resources and HTTP verbs as operations. RPC just picks different primitives.

An RPC contract describes services and methods:

syntax = "proto3";

package orders.v1;

service OrderService {
rpc CreateOrder(CreateOrderRequest) returns (Order);
rpc GetOrder(GetOrderRequest) returns (Order);
rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse);
rpc CancelOrder(CancelOrderRequest) returns (Order);
rpc WatchOrder(WatchOrderRequest) returns (stream OrderEvent);
}

Why It Matters Here

REST and RPC each win in particular contexts. The decision is not "which is better" but "which matches the traffic shape and consumer profile":

  • REST wins when consumers are heterogeneous, tooling-poor, or browser-based, and when caching matters
  • gRPC wins inside a polyglot microservices estate where performance, streaming, and typed clients matter more than human readability
  • JSON-RPC wins when you want "REST without the ceremony" for a small internal service

Picking the wrong style imposes cost forever: gRPC across a public internet with 50 external consumers means shipping SDKs and generated clients in every language; REST across 200 internal microservices means serializing JSON where binary would do.

Concrete Example

Same operation in three styles.

REST:

POST /orders
Idempotency-Key: 4b2e...
Content-Type: application/json

{ "customer_id": "c_9", "items": [ { "sku": "s_1", "quantity": 2 } ] }

HTTP/1.1 201 Created
Location: /orders/ord_1
Content-Type: application/json

{ "id": "ord_1", "customer_id": "c_9", "total": 4599, "status": "pending", ... }

gRPC (.proto):

message CreateOrderRequest {
string customer_id = 1;
repeated LineItem items = 2;
string idempotency_key = 3;
}

message LineItem {
string sku = 1;
int32 quantity = 2;
}

message Order {
string id = 1;
string customer_id = 2;
int64 total_minor = 3;
OrderStatus status = 4;
google.protobuf.Timestamp created_at = 5;
}

enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_PAID = 2;
ORDER_STATUS_CANCELLED = 3;
}

The client call (generated):

response = stub.CreateOrder(CreateOrderRequest(
customer_id="c_9",
items=[LineItem(sku="s_1", quantity=2)],
idempotency_key="4b2e...",
))
print(response.id, response.status) # fully typed

Bidirectional streaming (hard in REST, natural in gRPC):

service Chat {
rpc Converse(stream Message) returns (stream Message);
}

JSON-RPC:

POST /rpc
Content-Type: application/json

{
"jsonrpc": "2.0",
"id": 17,
"method": "orders.create",
"params": {
"customer_id": "c_9",
"items": [ { "sku": "s_1", "quantity": 2 } ]
}
}

HTTP/1.1 200 OK
Content-Type: application/json

{
"jsonrpc": "2.0",
"id": 17,
"result": { "id": "ord_1", "status": "pending", "total": 4599 }
}

Compared to REST, notice:

  • no verbs or URL hierarchy; method is the whole routing
  • responses are always 200 OK - errors go in an error object inside the body
  • batching is native: send an array of JSON-RPC objects

Common Confusion / Misconception

"gRPC is just faster JSON." gRPC's binary wire format helps, but the real wins are (1) a schema you cannot drift from, (2) code-generated clients for 10+ languages, and (3) built-in streaming. If you need none of those, REST is fine.

"gRPC doesn't work in browsers." It did not for a long time. gRPC-Web exists now but adds a proxy. For public, browser-facing APIs, REST or GraphQL is still the safer default.

"JSON-RPC is dead." It is small and quiet, which is a feature. For a wallet service, a blockchain node, or a focused admin API, JSON-RPC's minimalism beats REST's overhead.

"All internal services should be gRPC." Consider team familiarity, tooling (observability, API gateway support), and debuggability first. A team that cannot read .proto-generated stack traces will waste weeks on their first gRPC outage.

"Status codes are a REST-only concept." gRPC has its own status codes (OK, NOT_FOUND, INVALID_ARGUMENT, UNAVAILABLE, DEADLINE_EXCEEDED). They are fewer and more semantically precise than HTTP codes. Use them correctly - they drive retry policy in generated clients.

How To Use It

Decision checklist for an integration:

  1. Who are the consumers? Heterogeneous external teams with varied tooling -> REST or GraphQL. Internal polyglot services the same team owns -> gRPC is cheap.
  2. What is the shape of the traffic? Request/response -> any. Streaming -> gRPC. Occasional small RPCs -> JSON-RPC.
  3. Do you need caching at the edge? Yes -> REST (HTTP caching is free). No -> RPC is fine.
  4. Do you control both ends? Yes -> RPC wins on tooling. No (public API) -> REST is far kinder to strangers.
  5. Do you have an API gateway? Most gateways speak HTTP natively and bolt gRPC on with translation. Check before committing.

In mixed estates, "REST at the edge, gRPC inside" is a common, workable compromise.

Check Yourself

  1. Why does gRPC's use of .proto schemas make versioning easier than ad-hoc REST JSON?
  2. When would you pick JSON-RPC over REST? Give one realistic scenario.
  3. You have an internal service with 5 methods, called by 3 other teams in the same language. Which style would you pick, and why?

Mini Drill or Application

Take the API you designed in Cluster 2 (REST) and restate it as gRPC:

  1. Write the service block with all methods.
  2. Write the message definitions for CreateXRequest, UpdateXRequest, and the main X entity.
  3. Map REST status codes to gRPC status codes for three common errors.
  4. Identify one method that benefits from gRPC streaming and explain why.

Write a one-paragraph "REST vs gRPC" decision for this API defending the style you would actually ship.

Read This Only If Stuck