API types and architectures: what REST, GraphQL, gRPC, SOAP, webhooks and event streams cost to run and to change
Most lists of API styles end in an analogy. Put one order lookup through all of them instead, count the bytes, read each spec for caching and change rules, and the right style for each part of your system becomes a cost decision.
What are API types and architectures?#
API types sort interfaces by who may call them (public, partner, internal or composite), while architecture styles such as REST, GraphQL, gRPC, SOAP, webhooks and event streams decide how calls travel. So what is API architecture? Among API types and architectures, it is the pair of decisions behind every interface you run: who may call it, and what shape the call takes on the wire.
Because the two axes are independent, real API architectures combine them freely. For example, a public API can be REST or GraphQL. An internal API can be gRPC or a Kafka topic. Stoplight's guide to API types sorts them by "who has access", and that is the audience axis. Meanwhile, a specification sets the style axis. The owners are RFC 9110 for HTTP semantics, the GraphQL spec and grpc.io. Then come the W3C SOAP 1.2 Recommendation, the Standard Webhooks spec, RFC 6455 for WebSockets and the Apache Kafka docs.
API protocols and architectures are often listed together, but they are not the same thing. A protocol such as HTTP/2 carries bytes. Instead, a style decides what those bytes mean. Then the API formats on the wire (JSON, XML or binary Protocol Buffers) follow from the style.
So what is API based architecture in practice? It is a system whose parts talk only through these contracts. In short, every boundary in that system gets its own type and its own style. The different types of API architecture are tools for different boundaries.
What does the choice of API style cost you?#
The style you pick sets three running costs for as long as the API lives: bytes per call, how much traffic a cache can absorb, and what a breaking change costs. Choosing the style per boundary keeps each of those costs where it is cheapest to pay.
Bytes come first. In our 6 October 2026 test, we encoded one order: id 42, status shipped, total 12999 cents. It took 19 bytes as a gRPC message, 48 as REST JSON, 68 as a GraphQL response and 270 as a SOAP 1.2 envelope. Those counts exclude headers and compression.
Take an illustrative load of 10,000 lookups a second. At that rate, the 222-byte gap from our 6 October 2026 test between SOAP and REST adds 2.22 megabytes a second. That comes to about 192 gigabytes a day. The 29-byte gap from the same 6 October 2026 test between REST and gRPC adds about 25 gigabytes a day at that illustrative rate. Meanwhile, at 10 lookups a second both gaps are tiny.
What a style's bytes cost at your own call rate
Set your call rate and two per-message sizes from the 6 October 2026 test; it computes the extra bytes per call, a second and a day.
Extra gigabytes a day
191.8
- Extra bytes per call
- 222
- Extra bytes a second
- 2,220,000
The call rate is an illustrative default, not a measured load. Change it to your own.
Other costs work alike. First, a read that a content delivery network (CDN) can serve never reaches your servers. Second, a field you can retire without a new version never forces a client release. Finally, a push style adds a fourth line. On the Standard Webhooks example schedule, read on 6 October 2026, one failed delivery is retried for about 75 hours.
What are the four types of API?#
The four types of API are public, partner, internal and composite, and each one fixes who holds credentials, how slowly the contract may change and how much documentation it needs. Each audience maps to a boundary with its own change speed, and that is what narrows the style choice.
Stoplight's guide describes public APIs as "available for anyone to use with little to no restriction". Most still ask for a key. Because you cannot reach every caller, a public contract changes slowest. It also needs the fullest documentation. Partner APIs, in Stoplight's words, "exist somewhere between public and private APIs". They serve a named company for a set purpose. Therefore, you can email every caller before a change and agree the contract with each one.
In contrast, internal APIs serve a closed group, usually your own teams. Since you ship both sides, they can change fastest. They can also use a binary style that a browser would struggle with. Finally, Stoplight defines a composite API as one "that lets you hit multiple API endpoints on a single call". So it is a boundary of its own, often in front of a browser or mobile app.
In short, the answer to "what are the 4 types of API" is four boundaries. The types of APIs are boundaries, and that is how API types and architectures connect. Then name the boundary each of your own interfaces sits on before you pick a style for it.
| Type | Who calls it | What that means for change |
|---|---|---|
| Public | Anyone, with little to no restriction; most still ask for a key | Changes slowest and needs the fullest documentation, because you cannot reach every caller |
| Partner | A named company for a set purpose | You can email every caller before a change and agree the contract with each one |
| Internal | A closed group, usually your own teams | Changes fastest, since you ship both sides; a binary style is an option |
| Composite | One call that hits multiple endpoints | A boundary of its own, often in front of a browser or mobile app |
What does a minimal request look like in each style?#
The same order lookup is a GET in REST, a POST with a query document in GraphQL, a typed method call in gRPC and an XML envelope in SOAP 1.2. Seeing one lookup written four ways shows where each style puts the operation, the types and the metadata.
# 1. REST: a GET on the resource, cacheable under RFC 9110
curl -s https://api.example.com/orders/42 -H 'Accept: application/json'
# 2. GraphQL: a POST carrying the query document and its variables
curl -s https://api.example.com/graphql \
-H 'Content-Type: application/json' \
-H 'Accept: application/graphql-response+json' \
-d '{"query":"query($id:ID!){order(id:$id){id status totalCents}}","variables":{"id":"42"}}'
# 3. SOAP 1.2: a POST carrying an XML envelope in the SOAP namespace
curl -s https://api.example.com/soap \
-H 'Content-Type: application/soap+xml; charset=utf-8' \
-d '<?xml version="1.0"?><env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope"><env:Body><m:GetOrder xmlns:m="urn:example:orders"><m:id>42</m:id></m:GetOrder></env:Body></env:Envelope>'
# 4. gRPC: the contract is a .proto file; generated stubs make the call
cat > orders.proto <<'PROTO'
syntax = "proto3";
service OrderService {
rpc GetOrder (GetOrderRequest) returns (Order);
}
message GetOrderRequest { int64 id = 1; }
message Order { int64 id = 1; string status = 2; int64 total_cents = 3; }
PROTO REST puts the operation in the method and the noun in the URL. However, GraphQL puts both in a document inside the body. Then SOAP wraps the call in an envelope. The W3C SOAP 1.2 Recommendation of 2007 fixes its namespace as http://www.w3.org/2003/05/soap-envelope. In gRPC, the call is a method on a service. The grpc.io introduction says the client "has a stub" generated from that file. Also, on the wire, gRPC sends application/grpc over HTTP/2, per the gRPC protocol document.
A webhook is the REST request turned around. Your server POSTs a JSON event to a URL the partner gave you. Meanwhile, a WebSocket or a Kafka topic carries the same event as a message, with no request at all.
Which specs should you build each style from?#
Build each style from its own specification: RFC 9110 for REST, the GraphQL spec and its HTTP guidance, grpc.io with the protobuf encoding guide, and W3C SOAP 1.2. The list below was checked on 6 October 2026. Because every style has an owning document, building from it avoids the drift that tutorials bring in.
- RFC 9110, IETF, June 2022: methods, status codes and which responses a cache may reuse.
- GraphQL serving over HTTP: POST and GET handling and the response media type.
- GraphQL spec, October 2021: the schema language and field deprecation.
- gRPC core concepts: the service definition, deadlines and metadata.
- Protocol Buffers encoding: how field numbers and varints become bytes.
- gRPC over HTTP/2: the content type and the length prefix on each message.
- Standard Webhooks: the headers, signatures and retry schedule.
- Apache Kafka introduction: topics, partitions and how long events are kept.
How many bytes does the same answer cost in each format?#
In our 6 October 2026 test of one order lookup, a gRPC message took 19 bytes, REST JSON 48, GraphQL JSON 68 and a SOAP 1.2 envelope 270, before headers or compression. Binary Protocol Buffers are smallest because field numbers replace field names, and SOAP is largest because the envelope and namespaces repeat on every call.
Show data table
| Item | Value |
|---|---|
| gRPC (Protocol Buffers + 5-byte prefix) | 19 bytes |
| REST JSON | 48 bytes |
| GraphQL JSON | 68 bytes |
| WebSocket frame (JSON event) | 77 bytes |
| Webhook event (JSON) | 115 bytes |
| SOAP 1.2 envelope | 270 bytes |
The same order costs 19 bytes as gRPC and 270 as a SOAP 1.2 envelope.
The protobuf encoding guide explains the gap. There, each field is a record "consisting of the field number, a wire type and a payload". So the name total_cents never travels. In our 6 October 2026 test, the 14-byte message plus the 5-byte prefix from the gRPC protocol document makes 19. Meanwhile, REST JSON spells every key on every call, and GraphQL adds the data and order wrappers. Finally, SOAP carries an XML declaration, an envelope, a body and two namespaces. Only then does it say anything about the order.
However, these are per-message numbers. Compression narrows the text formats, while HTTP headers often outweigh a small body. So the ratio matters at volume, on chatty internal links and on mobile data. It barely matters on a dashboard called a few times a minute.
Which API styles can an HTTP cache serve without extra work?#
RFC 9110 (June 2022) lets a cache reuse GET responses with codes such as 200 and 404 (Not Found) by default, so REST reads cache freely while POST-based GraphQL, gRPC and SOAP calls do not. Requests sent as GET with cacheable status codes get CDN savings by default, and GraphQL regains them only with GET queries and persisted documents.
RFC 9110, published by the IETF in June 2022, lists the statuses a cache may reuse without explicit freshness headers. It closes the list plainly: "all other status codes are not heuristically cacheable". As a result, a public REST read can be served from the edge with a Cache-Control header and no code. Because a cache keys on the method and the URL, a POST body is invisible to it.
The GraphQL Foundation's guide says a server "must handle the HTTP POST method for query and mutation operations, and may also accept the GET method for query operations". Its performance page adds that GET requests "are typically considered cacheable by default". But long queries hit URL size limits. So the same page points to persisted queries, where the client sends "a hash of the query instead" and the server looks the document up. In practice, a cacheable GraphQL API is a project, not a default.
gRPC and SOAP sit further out. Since both send every call as a POST, a CDN passes them through untouched. Instead, their caching lives in your application, which is extra code and extra memory to run. Therefore, a read-heavy public API leans toward REST on caching alone.
How does each contract change without breaking clients?#
REST versions through URLs or media types, GraphQL deprecates fields in place, Protocol Buffers let old readers skip unknown field numbers, and SOAP changes the XML namespace. The cheapest contract to change is the one whose old clients keep working without a new version, which is GraphQL deprecation and protobuf field numbers.
The GraphQL spec lets a schema mark a field @deprecated. It then says "It is still legal to include these fields in a selection set (to ensure existing clients are not broken by the change)". So a field can be retired slowly while old clients keep running. Similarly, Protocol Buffers identify fields by number. The encoding guide describes an old server that "treats some fields as unknown" and still serializes the message. As a result, you can add a field without a release on the other side. The one rule is never to reuse a number.
REST has no built-in rule for this. Instead, teams put a version in the path. Others use content negotiation from RFC 9110 (June 2022), where the Accept header asks for a media type. Either way, a breaking change means two versions running until the last client moves. Meanwhile, SOAP ties the message to its namespace. So a new contract usually means a new namespace that every partner must adopt.
| Style | How the contract changes | Old clients keep working without a new version? |
|---|---|---|
| GraphQL | Mark a field @deprecated and retire it slowly | Yes: deprecated fields stay legal in a selection set |
| gRPC (Protocol Buffers) | Add fields under new numbers; never reuse a number | Yes: old readers treat new fields as unknown |
| REST | A version in the path, or a media type through the Accept header | No: two versions run until the last client moves |
| SOAP | A new XML namespace | No: every partner must adopt the new namespace |
How long does a failed webhook keep costing you?#
On the example schedule in the Standard Webhooks specification, read on 6 October 2026, a failed delivery keeps being retried for about 75 hours, so every receiver must accept duplicates by event id. A webhook is cheap to send but obliges both sides to run retries, signatures and idempotency for days after a failure.
Show data table
| Stage | Value |
|---|---|
| attempt 4 | 0.58 hours |
| attempt 5 | 2.58 hours |
| attempt 6 | 7.58 hours |
| attempt 7 | 17.58 hours |
| attempt 8 | 31.58 hours |
| attempt 9 | 51.58 hours |
| attempt 10 | 75.58 hours |
The tenth attempt lands about 75 hours after the first.
The spec is direct: "It's recommended to retry delivery following a retry schedule spanning multiple days, with an exponential backoff." Its example, read on 6 October 2026, waits 5 seconds, then 5 minutes, then 30 minutes, then hours. So the tenth attempt lands 75 hours and 35 minutes after the first. Meanwhile, the sender keeps a queue for that whole window. In turn, the receiver must check a signature in the webhook-signature header. It must also use the webhook-id header "as an idempotency key", because the same event can arrive twice.
In short, a webhook moves cost to both companies. That suits a partner boundary, where you cannot hold a connection open to someone else's servers. However, it is a poor fit inside your own system. There, an event log does the same job with replay built in.
What does an open connection or an event log cost to run?#
Under RFC 6455 (December 2011), a WebSocket frame adds only 6 to 14 bytes, but each client holds a connection open, while Apache Kafka keeps every event durably and lets consumers replay it. Streaming styles move the cost from bytes per message to infrastructure held open: connections for WebSockets, a replicated log for Kafka.
Show data table
| Item | Value |
|---|---|
| payload up to 125 bytes | 6 bytes |
| payload 126 to 65,535 bytes | 8 bytes |
| payload over 65,535 bytes | 14 bytes |
A client frame adds 6 to 14 header bytes, depending on the payload size.
RFC 6455 (IETF, December 2011) says "The WebSocket Protocol enables two-way communication between a client" and a server over one connection. Each client frame carries a 2-byte header and a 32-bit masking key, plus extra length bytes for larger payloads. That is cheap per message. Instead, the cost is the open socket, because a server holds one per connected browser. Similarly, the gRPC performance guide notes that each HTTP/2 connection "usually has a limit on the number of concurrent streams".
Kafka trades sockets for a log. Its introduction names one goal plainly: "To store streams of events durably and reliably for as long as you want." Events "can be read as often as needed", and each topic sets its own retention. Also, topics are split into partitions across brokers and can be replicated. So that is a cluster to run, watch and upgrade. In return, a new consumer can replay last week's orders without asking anyone to resend them.
How do you choose an API style for each boundary of your system?#
Pick the style per boundary: REST for public reads that should cache, GraphQL for one team's varied clients, gRPC between internal services, webhooks to partners and an event log inside. A boundary's audience, cache need and team skills decide its style, and one system usually ends up with two or three styles.
| Boundary | Style | Why, from the costs above | Skills the team needs |
|---|---|---|---|
| Public read API | REST | GET responses cache by default (RFC 9110) | HTTP semantics, cache headers, versioning |
| One team's web and mobile clients | GraphQL | Clients pick fields; deprecation keeps old ones working | Schema design, resolvers, persisted queries |
| Internal service to service | gRPC | Smallest message in our test; typed stubs | Protocol Buffers, generated stubs, deadlines |
| Push to a partner | Webhooks | No open connection to another company | Retries, signatures, idempotency keys |
| Browser live updates | WebSocket | 6 to 14 header bytes per frame | Connection limits, reconnect logic |
| Internal events many teams read | Kafka topic | Durable, replayable log | Running a broker cluster, partitions, retention |
| Existing enterprise partner on SOAP | SOAP 1.2 | The partner already runs it | XML envelopes, namespaces, faults |
Source: the specs and docs cited above; byte figures from our 6 October 2026 test.
Then check the team. The grpc.io introduction expects you to define services in a .proto file and generate code. So someone must own those files. Likewise, a GraphQL schema needs an owner for deprecations. Also, webhooks need someone on call for the retry queue. When a style has no owner, its costs land on whoever is nearest.
For example, a shop might run REST for its public catalogue and GraphQL behind its own app. It might add gRPC between checkout and stock, webhooks to its carriers and a Kafka topic for orders. That is five styles, each where it is cheapest.
When is adding a new API style the wrong fit?#
Adding a second style is the wrong fit when one client calls the API, the traffic is small, or nobody on the team will own the schema, the proto files or the retry queue. Keep the style you already run unless a measured cost is large enough to pay for a second toolchain. That cost shows up as bytes, cache misses or retries.
The 29-byte gap from our 6 October 2026 test between gRPC and REST is real. At an illustrative 10 calls a second, though, it comes to 290 bytes a second. In that case, the better tool is the REST API you have, with compression switched on. Likewise, take a single web app as the only client. There, a GraphQL layer adds a schema and resolvers to solve a problem you do not have. Plain REST endpoints shaped for that app are simpler.
Also, if you only need to tell a browser that something changed, a held-open WebSocket may be more than you need. For example, our web push guide shows a push channel with no socket to keep open. Finally, if a partner already runs SOAP, keep SOAP at that boundary rather than asking them to rebuild.
Where should you go from here?#
If you are splitting a monolith, decide the service boundaries first, then give each one the style its audience needs. Because the boundary decision comes before the style, the next reads start there.
Start with when a module should become a service, then how teams split ownership of a front end. For a REST integration in practice, see cart updates with payment intents. If you want help designing or connecting an API, our API integration and custom software development teams do that work. Still, the specs linked above are enough on their own to make every choice here.