JSON REST vs GraphQL vs gRPC: Performance, DX, and When to Use Each

Last updated:

JSON REST, GraphQL, and gRPC are the three protocols you actually choose between when you ship a new service in 2026. REST is JSON over HTTP — text wire, predictable URLs, works everywhere, debuggable with curl. GraphQL is also JSON over HTTP but with a schema and a query language that lets clients request exactly the fields they need — optimized for read-heavy, multi-client systems. gRPC is binary Protobuf over HTTP/2 — the fastest of the three for service-to-service traffic, but it requires a proxy for browsers and a code-generation step in your build. The right answer is almost never "pick one for the whole company" — it is "pick one per use case based on who the clients are, what the payloads look like, and what your team is good at."

Debugging a JSON REST or GraphQL response and the body looks malformed? Paste it into Jsonic's JSON Validator — it pinpoints the exact byte where parsing breaks, with line and column numbers.

Validate a JSON response

TL;DR decision matrix (one paragraph + table)

Pick REST when clients are external developers, browsers, or partners; payloads are resource-shaped; and the team has not standardized on codegen tooling. Pick GraphQL when you have multiple client teams with different field needs, read traffic spans relationships, and over-fetching is a measurable problem. Pick gRPC for internal service-to-service traffic where you control both ends, latency or payload size is a constraint, and the team is comfortable with .proto files and per-language code generation. Most production systems run two of the three side by side — REST or GraphQL on the public edge, gRPC between internal services.

DimensionJSON RESTGraphQLgRPC
Wire formatJSON textJSON text (query-shaped)Binary Protobuf
Typical latency5–50 ms10–60 ms1–5 ms
Typical payload80–200 KB20–80 KB5–30 KB
Browser supportNativeNativeNeeds gRPC-Web + proxy
SchemaOpenAPI (optional)SDL (required).proto (required)
CodegenOptionalCommonRequired
StreamingSSE / WebSocketSubscriptions (WS)Native bidirectional
Error handlingHTTP status codes200 + errors[] arraygRPC status codes
ToolingPostman, curl, SwaggerGraphiQL, Apollo Studiogrpcurl, Buf Studio
Public API friendlinessExcellentGoodPoor
HTTP cache compatibilityExcellent (GET URLs)Poor (POST)Custom

Wire format: JSON text vs JSON over HTTP vs Protobuf binary

The wire is where the three protocols diverge most concretely. JSON REST sends UTF-8 JSON text in the request and response body — every field name is repeated as a string, every number is encoded as decimal text, every boolean is the literal wordtrue or false. Gzip helps (often 60–80% reduction on JSON), but never matches binary efficiency. A typical user profile response from a REST endpoint runs 1–4 KB compressed, 4–20 KB uncompressed.

GraphQL ships the same JSON wire format — the response body is a JSON object with adata key and optional errors array. The compactness win comes from query shape: the client asked for three fields, the response has three fields, the over-fetching tax that REST endpoints often pay disappears. The parsing cost on both ends is the same as REST because the format is the same.

gRPC sends Protocol Buffers — a length-prefixed binary format where field names are compiled into small integer tags, integers use variable-length encoding, and there is no whitespace, no quotes, no punctuation. A field that takes 32 bytes as"user_id": "abc-123-def-456" in JSON takes ~18 bytes as Protobuf. For high-throughput service traffic the wire bytes matter — not for end-user latency, but for bandwidth costs and serialization CPU at scale.

# Same data on the wire, three encodings

# JSON (REST or GraphQL response body) — 84 bytes
{"id":42,"name":"Ada Lovelace","active":true,"score":98.6}

# Protobuf (gRPC) — 23 bytes, hex view
08 2a 12 0c 41 64 61 20 4c 6f 76 65 6c 61 63 65
18 01 25 33 33 c5 42

# Field 1 (id=42) → 08 2a
# Field 2 (name="Ada Lovelace") → 12 0c + 12 ASCII bytes
# Field 3 (active=true) → 18 01
# Field 4 (score=98.6) → 25 + 4-byte float

See our JSON vs Protobuf binary guide for the byte-level encoding rules.

Schema and typing: OpenAPI vs SDL vs .proto

All three protocols support typed schemas, but the role the schema plays differs. OpenAPI is optional for REST — many REST APIs ship without one, and the schema is a separate document that describes paths, methods, parameters, and response shapes. You can run a REST API with no schema at all; tooling like Swagger UI and code generators are nice-to-haves. SDL (the GraphQL Schema Definition Language) is mandatory — every GraphQL server publishes its schema through introspection, and clients can query the schema itself to discover available fields and types. The .proto file is mandatory for gRPC — service and message definitions live in .proto, and the build step generates server stubs and client code per language.

# OpenAPI 3.1 (REST) — describes existing endpoints
paths:
  /users/{id}:
    get:
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'

# GraphQL SDL — defines the schema, queries are written against it
type User {
  id: ID!
  name: String!
  active: Boolean!
  score: Float
}
type Query {
  user(id: ID!): User
}

# Protocol Buffers (.proto) — gRPC service and message definitions
syntax = "proto3";
service Users {
  rpc GetUser(GetUserRequest) returns (User);
}
message GetUserRequest { string id = 1; }
message User {
  string id = 1;
  string name = 2;
  bool active = 3;
  double score = 4;
}

For more on REST schema design see our JSON REST API design guide, and for the JSON:API spec specifically see our JSON:API guide.

Performance: latency, throughput, payload size (with real numbers)

Performance comparisons are loaded with caveats — hardware, network, payload shape, and serialization library all matter — but the order of magnitude is stable across benchmarks. For a representative read of a user record (~50 fields, mixed types) under steady load on the same hardware:

  • gRPC unary call: 1–5 ms p50, 5–30 KB on the wire, HTTP/2 multiplexed (many concurrent calls on one connection)
  • GraphQL query (selected fields): 10–60 ms p50, 20–80 KB on the wire — JSON parsing dominates server-side cost, resolver fan-out adds variance
  • JSON REST endpoint: 5–50 ms p50, 80–200 KB on the wire if the endpoint is verbose, much smaller if the endpoint is purpose-built

gRPC's 30–50% latency win in microbenchmarks comes from three sources: binary encoding is faster to parse than JSON, HTTP/2 header compression and stream multiplexing remove per-request overhead, and connection reuse eliminates TLS handshakes between calls. The win shrinks when you add real-world overhead — TLS termination at a load balancer, function cold starts, database queries that take 20 ms — but it never disappears.

GraphQL's N+1 problem is a known performance trap: a query that asks for posts and their authors can fire one query for posts plus one query per author. The fix is DataLoader (or its equivalent in your language) — a per-request cache that batches identical key lookups into a single database query. Without DataLoader, GraphQL latency can be far worse than the equivalent REST endpoint; with it, performance is competitive.

For browser-facing latency — the only number end users feel — the wire format difference shrinks to single-digit milliseconds. Network round-trip, TLS handshake, and JavaScript parsing dominate. The choice between REST and GraphQL on the public edge is rarely a latency decision.

Browser support: REST and GraphQL native, gRPC needs gRPC-Web

REST and GraphQL run natively in every browser — fetch()works out of the box, request and response bodies appear in DevTools as parsed JSON, and any HTTP debugging proxy (Charles, mitmproxy, the browser's own Network tab) shows the full conversation. There is no client library required for a basic call.

gRPC does not run natively in browsers — the protocol requires HTTP/2 trailers, which browsers do not expose to JavaScript. The workaround is gRPC-Web, a slightly different protocol that uses base64-encoded Protobuf bodies over HTTP/1.1 or HTTP/2 plus a proxy that translates between gRPC-Web and real gRPC. The two mainstream proxies are Envoy (with its grpc_web filter) and Connect by Buf (a single server that speaks gRPC, gRPC-Web, and the Connect protocol). The client side adds ~80 KB to your bundle for the gRPC-Web runtime plus your generated client code.

Connect-Web by Buf is the modern alternative — it speaks the Connect protocol directly to a Connect server, skips the gRPC-Web translation, and ships a smaller client. If you want gRPC ergonomics in a browser without an Envoy hop, Connect is the path most teams pick in 2026.

# curl a REST endpoint — works with no setup
curl https://api.example.com/users/42

# Make a GraphQL query with curl
curl https://api.example.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query": "{ user(id: \"42\") { id name active } }"}'

# grpcurl — like curl but for gRPC (requires .proto or reflection)
grpcurl -proto users.proto \
  -d '{"id": "42"}' \
  api.example.com:443 users.Users/GetUser

Tooling: Postman/Swagger vs GraphiQL vs grpcurl, codegen

REST has the largest tooling ecosystem by a wide margin. Postman and Insomnia ship REST-first GUIs with collections, environment variables, and shared workspaces. Swagger UI renders OpenAPI as an interactive console. curl works at the command line. Browser DevTools shows everything. SDK generation is optional — OpenAPI Generator produces clients in 50+ languages from a single OpenAPI spec.

GraphQL has a small but excellent toolset focused on the schema. GraphiQL (open source) and Apollo Studio (hosted) give you a query editor with autocomplete powered by introspection — type a field name and the tool offers every valid child field. The Apollo client and urql for the web, plus Relay for React, handle caching, optimistic updates, and pagination. Codegen tools (graphql-codegen) turn your schema into typed TypeScript clients.

gRPC tooling is smaller and more codegen-centric. grpcurl is the command-line client — it can read .proto files directly or use server reflection if the server enables it. Buf is the modern .proto build system with linting, breaking-change detection, and a registry for shared schemas.Buf Studio is a hosted GUI roughly equivalent to Postman for gRPC. The codegen step is mandatory — every language has its own plugin (protoc-gen-go, protoc-gen-ts, protoc-gen-python) that turns .proto into typed client and server code.

Streaming: SSE/WebSocket vs subscriptions vs bidirectional

All three protocols can stream, but the mechanics differ. REST handles streaming with Server-Sent Events (one-way, server-to-client, HTTP/1.1 compatible, text-only) for things like progress updates and live notifications, orWebSocket (full duplex, separate protocol upgrade, binary-capable) for chat and collaborative editing. Both are well-supported in browsers; neither is built into the REST mental model — they sit alongside it.

GraphQL has subscriptions, a schema-first streaming primitive typically delivered over WebSocket using the graphql-ws protocol. Subscriptions look like queries with a different operation type and stream JSON payloads matching the subscription's selection set. They are excellent for real-time UIs (chat, live dashboards) where the client already speaks GraphQL. The server side is more involved — you maintain a pub/sub layer (often Redis) that feeds subscription resolvers.

gRPC supports four call patterns natively: unary (request → response), server streaming (request → response stream), client streaming (request stream → response), and bidirectional streaming (full duplex over the same HTTP/2 stream). Bidirectional streams are the most flexible streaming primitive of the three protocols — long-lived, multiplexed, low-overhead — and they work without a separate WebSocket upgrade.

Migration paths: REST → GraphQL, REST → gRPC, polyglot architectures

REST → GraphQL is the most common migration. The pragmatic path is additive: stand up a GraphQL endpoint as a thin layer over your existing REST API, with resolvers that call REST routes and reshape the response. This gives clients the GraphQL query benefits without rewriting your data layer. Migrate clients screen by screen. Once GraphQL has absorbed enough of the read traffic, the underlying REST endpoints can be replaced with direct database resolvers — or kept indefinitely if they are stable. See our JSON in GraphQL guide for the wire-format details.

REST → gRPC is rarer and almost always internal-only. The trigger is usually a hot internal path where JSON parsing or payload size shows up in profiles — a high-QPS service-to-service call where 30% latency savings is worth the codegen tooling investment. Define the .proto, generate server stubs in your service language, swap the REST handler for the gRPC handler, and update clients. The external REST API stays where it is — many teams run both, with gRPC between services and REST or GraphQL at the edge. See our JSON-RPC vs REST guide for the older JSON-RPC alternative.

Polyglot architectures are the rule, not the exception, in 2026. The common shape: GraphQL or REST at the edge for browser and partner traffic, gRPC between internal services, and event streams (Kafka, NATS) for async work. Each protocol earns its place where it fits and stays out of the way elsewhere. The mistake is forcing one protocol across the whole system because of ideology — every public API on gRPC, every internal call on REST, every screen on GraphQL — that ends in custom proxies, awkward fallbacks, and rebuilt tooling.

For full-stack TypeScript apps where both client and server ship together, see our tRPC JSON wire format guide — tRPC is a fourth option that drops the schema language entirely in favor of TypeScript type inference.

The same endpoint in REST, GraphQL, and gRPC

Same data — get a user by id — three implementations. Each is a minimal working version; in production you would add auth, validation, logging, and tests.

// REST with Express (JSON over HTTP)
import express from 'express'
const app = express()

app.get('/users/:id', async (req, res) => {
  const user = await db.users.findById(req.params.id)
  if (!user) return res.status(404).json({ error: 'Not found' })
  res.json(user)
})

app.listen(3000)
// GraphQL with Apollo Server (JSON over HTTP, query-shaped)
import { ApolloServer } from '@apollo/server'

const typeDefs = `
  type User { id: ID!, name: String!, active: Boolean!, score: Float }
  type Query { user(id: ID!): User }
`

const resolvers = {
  Query: {
    user: async (_, { id }) => db.users.findById(id),
  },
}

const server = new ApolloServer({ typeDefs, resolvers })
await server.start()
// gRPC with Node.js (binary Protobuf over HTTP/2)
// users.proto
syntax = "proto3";
service Users {
  rpc GetUser(GetUserRequest) returns (User);
}
message GetUserRequest { string id = 1; }
message User {
  string id = 1;
  string name = 2;
  bool active = 3;
  double score = 4;
}

// server.ts
import * as grpc from '@grpc/grpc-js'
import { loadPackageDefinition } from '@grpc/grpc-js'
import { loadSync } from '@grpc/proto-loader'

const proto = loadPackageDefinition(loadSync('users.proto')) as any

const server = new grpc.Server()
server.addService(proto.Users.service, {
  GetUser: async (call: any, callback: any) => {
    const user = await db.users.findById(call.request.id)
    if (!user) return callback({ code: grpc.status.NOT_FOUND })
    callback(null, user)
  },
})

server.bindAsync(
  '0.0.0.0:50051',
  grpc.ServerCredentials.createInsecure(),
  () => server.start()
)

The REST version is the shortest. The GraphQL version adds a schema but otherwise looks similar. The gRPC version is the longest because of the .proto file and the protoc-driven setup — that cost amortizes once you have many services.

# Calling each one — same logical operation, three protocols

# REST
curl https://api.example.com/users/42

# GraphQL (POST a query)
curl https://api.example.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ user(id:\"42\") { id name active score } }"}'

# gRPC
grpcurl -proto users.proto \
  -d '{"id":"42"}' \
  api.example.com:443 users.Users/GetUser

Key terms

REST (Representational State Transfer)
An HTTP-based architectural style where resources have URLs and operations map to HTTP methods (GET, POST, PUT, DELETE). When people say "REST API" in 2026 they almost always mean JSON over HTTP with conventional URL shapes — the formal REST constraints (HATEOAS, layered system) are rarely enforced.
GraphQL
A query language and runtime for APIs. Clients send a query that specifies exactly which fields to return; the server executes a resolver per field and returns a JSON response shaped to the query. One endpoint serves all reads and mutations.
gRPC
A binary RPC framework using Protocol Buffers over HTTP/2. Services and messages are defined in .proto files; a protoc build step generates typed client and server code per language. Supports unary calls and three streaming modes (server, client, bidirectional).
Protocol Buffers (Protobuf)
A binary serialization format from Google. Field names are compiled to small integer tags, integers use variable-length encoding, and there is no whitespace or punctuation. Typically 3–10× smaller than the equivalent JSON.
DataLoader
A per-request batching and caching utility used in GraphQL resolvers to avoid the N+1 problem. Multiple resolver calls asking for the same kind of key in the same tick are combined into one batched database query.
gRPC-Web
A variant of the gRPC protocol that browsers can speak. Uses base64-encoded Protobuf over HTTP/1.1 or HTTP/2 and requires a proxy (Envoy, Connect, or grpcweb-proxy) to translate between gRPC-Web and real gRPC.
tRPC
A TypeScript-first RPC library that sends JSON over HTTP and shares types between server and client via TypeScript type inference — no .proto file, no schema language, no codegen. Unrelated to gRPC despite the similar name.

Frequently asked questions

Which is faster — REST, GraphQL, or gRPC?

gRPC is the fastest of the three for service-to-service traffic. A typical gRPC call runs 1–5 ms with payloads of 5–30 KB because the wire format is binary Protobuf and the transport is HTTP/2 with header compression and stream multiplexing. A JSON REST endpoint over HTTP/1.1 or HTTP/2 usually sits at 5–50 ms with 80–200 KB JSON bodies for the same data — the wire is text, gzip helps but never matches Protobuf compactness, and parsing JSON is significantly slower than parsing Protobuf. GraphQL sits between the two: the wire is JSON so parsing cost matches REST, but query-shaped responses are commonly 50–80% smaller than the equivalent REST endpoint because clients only request the fields they need. For browser-facing traffic the gRPC advantage shrinks — gRPC-Web adds an Envoy or Connect proxy hop, and end-user latency is dominated by TLS, geographic distance, and middleware rather than wire format.

Should I use gRPC for a public API?

Usually no. gRPC is a poor fit for public APIs for three reasons. First, browsers cannot speak native gRPC — gRPC-Web requires a proxy (Envoy, Connect, grpcweb-proxy), which means your public consumers either need that proxy in their stack or you publish a parallel REST/JSON layer anyway. Second, debugging is harder for outside developers — binary Protobuf payloads do not show up in browser DevTools as human-readable text, and curl needs grpcurl with the .proto file to make a single call. Third, schema distribution is awkward — you must publish .proto files and consumers must regenerate clients on every change. Use gRPC for internal service-to-service traffic where you control both ends, you ship the .proto file, and the latency or payload-size wins are real. For public APIs prefer JSON REST or GraphQL — both work natively in browsers, are debuggable with standard tools, and expose schemas through OpenAPI or SDL introspection.

When should I switch from REST to GraphQL?

Switch to GraphQL when three signals appear together. First, your clients are over-fetching — your mobile app needs five fields from an endpoint that returns fifty, your dashboard needs different field sets per screen, and you keep adding ?fields= or ?include= query parameters to your REST routes. Second, you have data-aggregation problems — clients are making three or four sequential REST calls to render one screen because relationships span endpoints. Third, you have multiple client teams (web, iOS, Android, partner integrations) that want different shapes and you cannot keep up by adding REST variants. GraphQL solves all three. The cost is real: you take on a query parser, a resolver layer with potential N+1 problems (use DataLoader), and a different mental model for caching. Do not switch for one client or one screen — REST plus a thin BFF (backend for frontend) layer is often the smaller change. Switch when GraphQL pays for itself across the whole portfolio.

Is gRPC binary or JSON?

gRPC is binary by default — the wire format is Protocol Buffers (Protobuf), a length-prefixed binary encoding that is typically 3–10× smaller than the equivalent JSON. A 200-byte JSON object often serializes to 30–50 bytes of Protobuf because field names are encoded as small integer tags rather than repeated strings, integers use variable-length encoding, and there is no whitespace or punctuation. gRPC also supports a JSON transcoding mode where the same .proto-defined service can be called with JSON bodies over HTTP — this is what Google Cloud APIs and Envoy gRPC-JSON transcoding use to expose a gRPC service as a REST/JSON endpoint without writing the REST layer twice. Some teams use this mode during development for easier debugging while keeping the binary protocol in production. See our JSON vs Protobuf binary guide for the byte-level comparison.

Does GraphQL replace REST?

No — GraphQL replaces a specific REST pattern, not REST itself. GraphQL excels at read-heavy, relationship-dense, multi-client data fetching: one endpoint, declarative queries, exactly the fields the client asked for. REST still wins for resource CRUD with predictable shapes (create user, update order, delete invoice), file uploads (multipart/form-data is awkward in GraphQL), HTTP caching (REST URLs cache cleanly in CDNs and browsers; GraphQL POST queries do not), and public APIs where developers expect curl plus a Postman collection. Many production systems run both: a REST API for mutations, file uploads, and webhooks, plus a GraphQL endpoint for read-side aggregation across multiple resources. The choice is per-use-case, not whole-system. Teams that go all-in on GraphQL often end up rebuilding HTTP cache semantics, file-upload handlers, and a REST-like surface for partners — at which point REST plus a BFF would have been simpler.

Can I run gRPC in a browser?

Not native gRPC — browsers do not expose HTTP/2 trailers to JavaScript, which gRPC requires. You can run gRPC-Web, a slightly different protocol that uses HTTP/1.1 or HTTP/2 with base64-encoded Protobuf bodies and a proxy that translates between gRPC-Web and real gRPC. The two mainstream paths are Envoy with its gRPC-Web filter or Connect by Buf (which speaks gRPC, gRPC-Web, and Connect protocol from a single server). The cost is a ~80 KB JavaScript client bundle for the gRPC-Web runtime plus your generated client code, plus the proxy in your deployment topology. For internal admin tools and dashboards this is fine. For public web traffic the bundle size and proxy hop usually push teams back to REST or GraphQL, both of which are first-class browser citizens with zero proxy overhead. If you want gRPC ergonomics in a browser without the proxy, Connect-Web speaks the Connect protocol directly to a Connect server and skips the gRPC-Web translation layer.

Which is easier to debug — REST, GraphQL, or gRPC?

REST is the easiest by a wide margin. URLs are human-readable, request and response bodies are JSON that browser DevTools render as a tree, curl works with no setup, and any HTTP proxy (Charles, mitmproxy, Vercel logs) shows everything. GraphQL is close behind for read traffic — GraphiQL and Apollo Studio give you query autocomplete from introspection, request and response are JSON, and DevTools shows the wire. The debugging cost is that all queries hit the same URL with POST, so HAR-file inspection requires opening the request body to see which operation ran. gRPC is the hardest of the three to debug ad-hoc — payloads are binary Protobuf, you need grpcurl with the .proto file to make a single test call, and DevTools shows opaque bytes. Teams running gRPC at scale invest in OpenTelemetry tracing, structured logs of decoded payloads, and tooling like Buf Studio. The debugging gap is the single biggest reason gRPC stays internal.

What about tRPC — is it the same as gRPC?

No — the names are similar but the technologies are unrelated. tRPC is a TypeScript-first RPC library that sends JSON over HTTP and shares TypeScript types directly between server and client via type inference — no .proto file, no code generation, no separate schema language. It works only when both ends are TypeScript, and the wire format is plain JSON (not Protobuf). gRPC is a language-agnostic binary RPC system using Protobuf and HTTP/2 — it works in any language with a gRPC implementation, requires explicit .proto definitions, and generates client code per language. tRPC trades the cross-language reach of gRPC for an extremely tight TypeScript developer experience: change a server function signature and the client gets a type error in the editor instantly. Use tRPC for full-stack TypeScript apps where both ends ship together. Use gRPC for polyglot service-to-service traffic where you need binary wire format and cross-language clients. See our tRPC JSON wire format guide for the deeper breakdown.

Further reading and primary sources