JSON-RPC vs REST: When to Use Each in 2026
Last updated:
REST models an API as a set of resources manipulated with HTTP verbs: GET /users/123 fetches a user, DELETE /users/123 removes it. JSON-RPC models an API as a set of procedure calls: the client sends {"jsonrpc": "2.0", "method": "getUser", "params": {"id": 123}, "id": 1} and the server returns a result or error. Both transport JSON, usually over HTTP — but JSON-RPC also runs natively over WebSocket, TCP, and stdio, which is why it dominates stateful and bidirectional protocols.
The honest summary up front: REST won the public-API war for good reasons, and you should default to REST for anything browser-, mobile-, or third-party-facing. JSON-RPC wins in three specific niches — stateful protocols (Ethereum, Language Server Protocol, Model Context Protocol), high-throughput RPC where batch matters, and bidirectional WebSocket APIs where the server pushes back. Pick by fit, not by fashion.
Building a JSON-RPC client and want to inspect request/response payloads? Paste them into Jsonic's JSON Formatter to pretty-print and syntax-highlight nested params.
Format JSON-RPC payloadSide-by-side request example
The clearest way to feel the difference is to look at the same operation expressed in both styles. Imagine an API that fetches user #123 and then updates their email.
REST
# Fetch
GET /users/123 HTTP/1.1
Host: api.example.com
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
{"id": 123, "name": "Ada", "email": "ada@example.com"}
# Update
PATCH /users/123 HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"email": "ada@new.example.com"}
HTTP/1.1 200 OK
Content-Type: application/json
{"id": 123, "name": "Ada", "email": "ada@new.example.com"}JSON-RPC 2.0
# Fetch
POST /rpc HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"jsonrpc": "2.0", "method": "getUser", "params": {"id": 123}, "id": 1}
HTTP/1.1 200 OK
Content-Type: application/json
{"jsonrpc": "2.0", "result": {"id": 123, "name": "Ada", "email": "ada@example.com"}, "id": 1}
# Update
POST /rpc HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"jsonrpc": "2.0", "method": "updateUserEmail", "params": {"id": 123, "email": "ada@new.example.com"}, "id": 2}
HTTP/1.1 200 OK
Content-Type: application/json
{"jsonrpc": "2.0", "result": {"id": 123, "name": "Ada", "email": "ada@new.example.com"}, "id": 2}Notice what REST gets you for free: the verb signals intent (read vs modify), the URL is a stable identifier you can bookmark and cache, and any HTTP tool — curl, browser devtools, a CDN — understands the semantics. Notice what JSON-RPC gets you: one URL, one verb (POST), and a method name that maps directly to a function in your code. For command-style operations like recalculatePortfolio or sendNotification, JSON-RPC's shape often feels more honest than shoehorning them into POST /portfolios/{id}/recalculations.
Comparison table: 10 dimensions
A balanced side-by-side. Where one approach is clearly better, the cell says so; where it's genuinely a tradeoff, the cell explains the tradeoff.
| Dimension | REST | JSON-RPC 2.0 |
|---|---|---|
| Semantic style | Resources + verbs (CRUD-shaped) | Procedure calls (command-shaped) |
| Transport | HTTP only | HTTP, WebSocket, TCP, stdio, named pipes |
| Error handling | HTTP status codes (4xx client, 5xx server) + body | HTTP 200 + error object with code + message |
| Batch requests | No spec-level batch (workarounds vary) | Built-in: array of requests, array of responses |
| Streaming / push | SSE or WebSocket bolt-on; not in REST itself | Server-initiated notifications over WebSocket/stdio |
| Caching | HTTP caching works out of the box (ETag, Cache-Control) | POST-only — no HTTP cache; must implement at app layer |
| Browser support | Native — every browser, every fetch library | Works (just POST), but no semantic browser tooling |
| Tooling | OpenAPI, Postman, curl examples, Swagger UI, API gateways | OpenRPC, LSP-style clients; narrower ecosystem |
| Schema / IDL | OpenAPI 3.x (mature, near-universal) | OpenRPC (newer, smaller adoption) |
| Idempotency | GET/PUT/DELETE are idempotent by spec | Up to you — protocol is silent |
| Discoverability | URL structure + HATEOAS hints | Method list + introspection if implemented |
| Real-world users | Stripe, GitHub, Twilio, almost every public web API | Ethereum, LSP, MCP, Bitcoin Core, Solana, Aria2 |
The two rows that decide most architectures are caching and batch. REST's HTTP caching is the single biggest reason it dominates public APIs — CDNs can serve GET /users/123from edge cache without touching your origin. JSON-RPC's batch is the single biggest reason high-frequency clients (blockchain indexers, IDEs, trading systems) prefer it — 100 calls in one round-trip beats 100 round-trips, full stop.
When REST wins: web APIs, mobile clients, public APIs
REST is the right default whenever any of these are true:
- Third parties consume your API. Every developer on Earth knows how to call
GET /users/123. They have curl muscle memory, Postman collections, and a favorite HTTP client. Asking them to learn your JSON-RPC method catalog is friction you don't want. - The operations are resource-shaped.If 80% of your API is Create/Read/Update/Delete on entities (users, orders, posts, files), REST's grammar fits the domain. Forcing it into
createUser/getUser/updateUserverbs is just renaming. - Caching matters.If your read traffic is 10-100x your write traffic — typical for content APIs, product catalogs, public data — HTTP caching at the CDN and browser layer is a massive win you don't get with JSON-RPC.
- Browser is a first-class client. Browsers have built-in semantics for HTTP verbs, status codes, CORS preflight, and credential handling. A REST API composes naturally with
fetch(), service workers, and the cache API. - You want commodity tooling. API gateways (Kong, AWS API Gateway, Cloudflare), observability tools (Datadog, New Relic), and rate-limit middleware all assume REST shapes. JSON-RPC works through them, but you lose per-method visibility unless you instrument it yourself.
Real-world examples that picked REST and don't regret it: Stripe (payments), GitHub (developer-facing), Twilio (messaging), every major cloud provider's management API. The pattern is clear — public, polyglot, cache-heavy, resource-shaped.
When JSON-RPC wins: WebSocket APIs, LSP-style protocols, batch RPC, blockchain
JSON-RPC is the better choice in four well-defined situations:
- Bidirectional WebSocket or stdio protocols.Language Server Protocol (used by VS Code, Neovim, JetBrains IDEs to talk to language servers) runs JSON-RPC over stdio. Model Context Protocol (MCP — used by Claude Code and other AI agents to talk to tool servers) runs JSON-RPC over stdio or HTTP+SSE. Both need the server to push messages back to the client (diagnostics, progress, tool results), and JSON-RPC's symmetric request/notification model handles that cleanly. REST has no equivalent — you'd need to bolt on SSE or WebSocket and invent your own message format.
- Blockchain RPC. Ethereum, Bitcoin Core, Solana, and every EVM fork expose JSON-RPC. Operations like
eth_call,eth_getLogs,eth_sendRawTransactionare verb-shaped, and the pub/sub case (eth_subscribe) needs WebSocket. The network effect is now self-reinforcing: any new chain that wants ecosystem compatibility ships an Ethereum-compatible JSON-RPC interface. - Batch-heavy clients. Indexers, ETL pipelines, and trading systems often need to fetch hundreds of items per second. JSON-RPC batch — send 100 calls, get 100 responses, one TCP round-trip — beats both sequential REST and parallel REST on tail latency and connection overhead. Etherscan, Alchemy, and Infura all expose batch endpoints because their power users demand it.
- Internal, command-shaped APIs.When you control both client and server and most operations are commands rather than resources (recalculate a report, trigger a workflow, apply a migration, send a notification), JSON-RPC's shape often maps more honestly to your code than REST's. Method name = function name. Params = function arguments. No URL design committee.
The common thread: JSON-RPC wins when you control both sides, when transport flexibility matters, or when the domain is naturally verb-shaped. It loses when the broader HTTP ecosystem (caches, gateways, browser tools) is doing work for you.
Error handling: HTTP status codes (REST) vs JSON-RPC error object
The two approaches encode errors in fundamentally different places. REST uses the HTTP status code as the primary signal; JSON-RPC uses an error object inside the 200-OK response body.
REST error
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"error": "user_not_found",
"message": "No user exists with id 123",
"documentation_url": "https://api.example.com/docs/errors#user_not_found"
}JSON-RPC error
HTTP/1.1 200 OK
Content-Type: application/json
{
"jsonrpc": "2.0",
"error": {
"code": -32004,
"message": "User not found",
"data": {"id": 123, "docs": "https://api.example.com/docs/errors"}
},
"id": 1
}The JSON-RPC 2.0 spec reserves error codes from -32768 to -32000 for protocol-level errors:
| Code | Meaning | When |
|---|---|---|
-32700 | Parse error | Invalid JSON received |
-32600 | Invalid Request | Valid JSON but not a valid JSON-RPC envelope |
-32601 | Method not found | The method does not exist |
-32602 | Invalid params | Method exists but params are wrong |
-32603 | Internal error | Server-side exception during execution |
-32000 to -32099 | Server error (reserved) | Implementation-defined |
Application-level errors (user not found, payment declined, validation failure) use any code outside -32768 to -32000. The tradeoff: REST's status-code approach is universally readable by HTTP middleware (a CDN knows 404 means don't cache); JSON-RPC's in-body approach is uniform and easier to extend with rich error data, but opaque to anything that only inspects HTTP headers.
Batch requests: where JSON-RPC has REST beat
JSON-RPC 2.0 makes batch a first-class feature. Send a JSON array of requests; the server returns a JSON array of responses. Notifications (requests without an id) are processed but not echoed in the response array.
POST /rpc HTTP/1.1
Content-Type: application/json
[
{"jsonrpc": "2.0", "method": "getUser", "params": {"id": 1}, "id": "a"},
{"jsonrpc": "2.0", "method": "getUser", "params": {"id": 2}, "id": "b"},
{"jsonrpc": "2.0", "method": "getOrders", "params": {"userId": 1}, "id": "c"},
{"jsonrpc": "2.0", "method": "logEvent", "params": {"name": "batch_fetch"}}
]
HTTP/1.1 200 OK
Content-Type: application/json
[
{"jsonrpc": "2.0", "result": {"id": 1, "name": "Ada"}, "id": "a"},
{"jsonrpc": "2.0", "error": {"code": -32004, "message": "Not found"}, "id": "b"},
{"jsonrpc": "2.0", "result": [{"orderId": 9001}], "id": "c"}
]Three things worth noting: (1) responses can come back in any order — clients match by id, not by position. (2) Each sub-response is independent — one failing call does not fail the batch. (3) The logEvent call is a notification (no id) so it has no entry in the response array.
REST has no equivalent. The common workarounds:
- Custom batch endpoint:
POST /batchwith an array of sub-request descriptors. Works, but bespoke per API and clients need adapter code. - HTTP/2 multiplexing: still N round-trips at the protocol level, just on one connection. Helps throughput but not tail latency.
- GraphQL: not a batch primitive, but you can fetch multiple things in one operation. Different paradigm with its own costs.
- JSON:API
?include=parameter: fetch related resources alongside the primary one, but only along defined relationships.
If your workload genuinely needs batch — high-volume reads from a single client, RPC fanout, blockchain indexing — JSON-RPC's native batch is a real advantage. For most APIs, the throughput gain is small enough that the cost (losing HTTP caching, learning a new protocol) outweighs the benefit.
Hybrid patterns: REST for CRUD, JSON-RPC for command-style endpoints
You don't have to pick one. A common, pragmatic pattern in production systems is REST for resource manipulation and JSON-RPC (or a single RPC POST endpoint) for command operations that don't fit the resource model.
# REST surface — resources
GET /api/users # list
POST /api/users # create
GET /api/users/123 # read
PATCH /api/users/123 # update
DELETE /api/users/123 # delete
GET /api/orders/456
GET /api/orders/456/items
# JSON-RPC surface — commands
POST /api/rpc
{"jsonrpc": "2.0", "method": "recalculatePortfolio", "params": {"portfolioId": "p_42"}, "id": 1}
{"jsonrpc": "2.0", "method": "sendInvoiceReminder", "params": {"invoiceId": "i_99"}, "id": 2}
{"jsonrpc": "2.0", "method": "applyDiscount", "params": {"orderId": 456, "code": "BLACK"}, "id": 3}The split mirrors a real distinction in your domain: resources (things that exist and have state) belong in REST; commands (operations that do work) belong in RPC. Forcing a recalculate-portfolio command into POST /portfolios/p_42/recalculations creates an awkward resource that exists only as a side effect of the verb.
When to actually do this: when you have a small number of commands that don't map cleanly to resources, and you don't want to pollute your REST namespace with verb-shaped URLs. When NOT to do this: when your team is small, your operations are 80%+ CRUD, and the cognitive cost of two protocols isn't worth it. Most teams should stay in pure REST and accept a few verb-shaped sub-resources.
Teams running this hybrid in production include Slack's Web API (REST-shaped URLs like chat.postMessagethat act like RPC method names — a third pattern entirely), Figma's plugin API (REST for files, RPC over WebSocket for live collaboration), and many internal microservice meshes (REST north-south, JSON-RPC east-west).
Key terms
- REST (Representational State Transfer)
- An architectural style defined by Roy Fielding's 2000 dissertation. Core constraints: stateless requests, uniform interface (HTTP verbs + URIs), client-server separation, cacheable responses, layered system. "REST API" in modern usage almost always means HTTP+JSON with resource-shaped URLs — a relaxed subset of Fielding's original spec.
- JSON-RPC
- A lightweight remote-procedure-call protocol that encodes calls as JSON objects with
jsonrpc,method,params, andidfields. Version 2.0 (2010) is the current spec — six pages, transport-agnostic, supports batch and notifications. Defined at jsonrpc.org. - Idempotent
- An operation that produces the same result whether called once or N times. In REST, GET/PUT/DELETE are idempotent by spec — clients and intermediaries can safely retry them. POST is not idempotent. JSON-RPC does not define idempotency at the protocol level; it is up to each method's contract.
- Batch request
- A single transport-level message containing multiple logical requests. JSON-RPC 2.0 supports batch natively: send a JSON array of request objects, receive a JSON array of response objects in one HTTP round-trip. REST has no spec-level batch.
- LSP (Language Server Protocol)
- A Microsoft-led protocol that lets editors talk to language servers (autocomplete, diagnostics, jump-to-definition) over JSON-RPC, usually via stdio. LSP is the reason every modern editor — VS Code, Neovim, JetBrains, Emacs — can support every language. The single most-deployed JSON-RPC application in the world.
- RPC (Remote Procedure Call)
- A pattern where a client invokes a procedure on a remote server as if it were a local function call. Protocol families: SOAP (XML), JSON-RPC (JSON), gRPC (Protocol Buffers over HTTP/2), Thrift, Cap'n Proto. All trade resource-orientation for verb-orientation.
Frequently asked questions
Should I use REST or JSON-RPC for a new API in 2026?
For a public web API consumed by third parties, mobile apps, or browsers, default to REST (or REST-flavored HTTP+JSON). The reasons are practical, not ideological: every HTTP client library, CDN, API gateway, observability tool, and caching layer is built around HTTP verbs and status codes. OpenAPI tooling, Postman collections, and curl examples all assume REST. JSON-RPC becomes the better choice when (a) you control both client and server, (b) most operations are commands rather than resource manipulations (sendEmail, recalculatePortfolio, applyMigration), (c) you need WebSocket or stdio transport, or (d) you need batch requests in a single round-trip. The honest answer most of the time: REST. The honest exceptions are real and growing — Ethereum, Language Server Protocol, and Model Context Protocol all chose JSON-RPC for good reasons.
Can JSON-RPC return HTTP 400 or only HTTP 200?
The JSON-RPC 2.0 spec is transport-agnostic — it does not mandate any specific HTTP status code. In practice, the dominant convention is to return HTTP 200 OK for every successfully-processed JSON-RPC envelope, including ones whose body contains an error object, because the HTTP request itself succeeded. A non-200 status is reserved for transport-level failures: malformed JSON in the request body returns 400, an unauthenticated client returns 401, the server being down returns 5xx. Ethereum nodes, LSP servers over HTTP, and most JSON-RPC libraries follow this pattern. Some implementations diverge — they map JSON-RPC error codes onto HTTP status codes — but this is non-standard and confuses generic HTTP middleware that expects 200 to mean success. When in doubt, return 200 with an error object in the body for application errors, and use HTTP status only for transport problems.
What is the difference between JSON-RPC and SOAP?
Both are RPC protocols that send a serialized procedure call over a transport, but they sit at opposite ends of the complexity spectrum. SOAP uses XML envelopes with verbose namespaces, mandates a WSDL contract, supports WS-Security/WS-Transaction/WS-* extensions, and typically runs only over HTTP with strict header conventions. A SOAP request for the same operation is often 5-10x larger than the JSON-RPC equivalent. JSON-RPC 2.0 is a six-page specification: a JSON envelope with method, params, id, and jsonrpc fields, transport-agnostic, no built-in schema or auth. SOAP fits regulated enterprise environments where formal contracts and WS-* features are required. JSON-RPC fits modern web, embedded, and developer-tool ecosystems where minimal overhead and easy debugging matter more. New projects almost never pick SOAP in 2026 — it survives mostly in legacy banking, insurance, and government systems.
Does JSON-RPC support streaming?
JSON-RPC 2.0 itself does not define streaming — every request gets exactly one response (or none, for notifications). However, JSON-RPC runs naturally over bidirectional transports like WebSocket and stdio, where the server can push unsolicited messages back to the client. Two common patterns simulate streaming: (1) Server-initiated notifications — after the client calls subscribe with a callback id, the server sends JSON-RPC notifications (requests with no id) to the client whenever new data is available. Ethereum uses this for eth_subscribe over WebSocket. (2) Chunked progress notifications — Language Server Protocol uses $/progress notifications to stream partial results during long-running operations like workspace indexing. Neither approach is in the core spec, but both are widely-supported conventions. If you need true streaming with backpressure (e.g., video, large file transfer), use a dedicated streaming protocol like gRPC, HTTP/2 SSE, or raw WebSocket frames, not JSON-RPC.
Why does Ethereum use JSON-RPC instead of REST?
Ethereum chose JSON-RPC in 2015 for three reasons that still hold up. First, blockchain operations are mostly verb-shaped, not resource-shaped: eth_sendTransaction, eth_call, eth_getBalance — these are procedures, not CRUD on /transactions/{id} resources. Forcing them into REST verbs creates awkward mappings. Second, every Ethereum client (Geth, Nethermind, Erigon, Reth) exposes the same RPC surface so wallets and dapps can swap providers without code changes — a verb-based interface is easier to standardize than a resource hierarchy. Third, the pub/sub use case (eth_subscribe to new blocks or log filters) needs a bidirectional transport, and JSON-RPC over WebSocket is the cleanest fit. The Ethereum JSON-RPC spec became the lingua franca of the EVM ecosystem: every L2, every fork, and every alt-chain that wanted EVM compatibility implements the same method names. That network effect now makes switching to REST impractical even if someone wanted to.
Can I do batch requests in REST?
Not natively. REST has no spec-level batch primitive — each request is one HTTP round-trip. Workarounds exist, all with tradeoffs: (1) Custom batch endpoint — POST /batch with an array of sub-requests in the body. Easy to implement but bespoke per API; clients need special code. (2) GraphQL — query multiple resources in one request, but you adopt a whole different paradigm. (3) HTTP/2 multiplexing — issue multiple parallel requests on the same connection; reduces overhead but is still N round-trips at the protocol level. (4) JSON:API includes — fetch related resources alongside the primary one via ?include=author,comments. None of these match the simplicity of JSON-RPC 2.0 batch: send an array, get an array back, one TCP round-trip. If batch is a first-class requirement, JSON-RPC has a real advantage — but for most REST APIs, the batch endpoint pattern is good enough.
Is GraphQL closer to REST or JSON-RPC?
Architecturally, GraphQL is closer to JSON-RPC than to REST. Like JSON-RPC: GraphQL ignores HTTP verbs (almost everything is POST /graphql), returns HTTP 200 even for application errors (with errors in the response body), supports a kind of batching (multiple operations per request), and is fundamentally a procedure-call protocol — your operation name selects which resolver to run. The big differences from JSON-RPC: GraphQL has a typed schema (SDL), the client specifies which fields it wants per request, and it has standard introspection. GraphQL trades JSON-RPC simplicity for declarative field selection and a strong type system. Practically, teams pick GraphQL when frontend needs vary widely and over-fetching is painful; they pick JSON-RPC when operations are clearly verb-shaped and a schema is overkill; they pick REST for everything else. All three are HTTP+JSON variations on the same theme.
What is the latency difference between REST and JSON-RPC?
For a single request, there is no meaningful latency difference between REST and JSON-RPC over HTTP — both send JSON over the same TCP/TLS connection, parse the same JSON body, and return the same JSON response. The wire overhead differs by tens of bytes (the JSON-RPC envelope adds jsonrpc, method, params, id keys) which is noise at any realistic payload size. Where JSON-RPC wins on latency is the batch case: 10 operations batched into one JSON-RPC request equals one TCP round-trip and one TLS handshake reuse, while 10 REST calls equal 10 round-trips (or up to 10 parallel streams on HTTP/2, which helps but is still 10 round-trip latencies). For a client 100ms from the server, batched JSON-RPC can save ~900ms over sequential REST. For a server on localhost, the difference is negligible. JSON-RPC over WebSocket also avoids per-request HTTP headers, which can save 200-1000 bytes per call — relevant for high-frequency RPC workloads like trading or blockchain nodes.
Further reading and primary sources
- JSON-RPC 2.0 Specification — The six-page official spec — envelope shape, error codes, batch, notifications
- Fielding Dissertation, Chapter 5: REST — Roy Fielding's original definition of the REST architectural style
- Ethereum JSON-RPC API — The reference RPC interface every EVM chain implements
- Language Server Protocol Specification — The most widely-deployed JSON-RPC application in the world
- OpenRPC vs OpenAPI — The OpenRPC project — schema/IDL for JSON-RPC, modeled on OpenAPI