KB KEDBYTE TECHNOLOGIES PRIVATE LIMITED
CHAPTER
36

APIs - What They Are and Where They Came From

Part G · Building and Shipping Software|28,273 words|about 123 min read|Volume 4

36.0 What this chapter gives you#

  1. You will be able to say what an API actually is in one sentence, and to defend that sentence: it is a published agreement, not a technology.
  2. You will be able to point at an API at five different scales, from one function inside one file up to a service on the other side of the planet.
  3. You will be able to tell the real history, with names and years: subroutine libraries in 1951, the term itself in 1968, remote procedure calls in 1984, CORBA in 1991, SOAP and REST around 2000, GraphQL and gRPC after 2015.
  4. You will be able to explain the difference between an API and an ABI, and say why a program can compile fine and still crash at run time.
  5. You will be able to read semantic versioning and say exactly what a change in each of the three numbers promises you.
  6. You will be able to list the eight fallacies of distributed computing and say what each one costs you when you believe it.
  7. You will be able to write out a raw HTTP request and response by hand, name every part, and pick the right status code for a situation.
  8. You will be able to state what Roy Fielding actually specified in 2000, and why almost nothing sold as REST today meets that specification.
  9. You will be able to compare REST, GraphQL, gRPC, SOAP, JSON-RPC, WebSockets and server-sent events on payload size, typing, streaming and tooling.
  10. You will be able to walk through the OAuth 2.0 authorization code flow with PKCE step by step, decode a JWT field by field, and say what a scope does.
  11. You will be able to make a client that does not fall over: retries with backoff and jitter, idempotency keys, timeouts and circuit breakers.
  12. You will be able to design an API other people can use, document it with OpenAPI, and explain why some companies live or die by that decision.

36.1 What an API actually is#

PLAIN36.1.1 in simple words#

  1. Software is made of parts. Parts need to ask each other to do things.
  2. An API is the published agreement about how one part may ask another part to do something.
  3. The letters stand for application programming interface. That name is not very helpful, so ignore the letters and remember the idea: an agreement.
  4. The agreement says three things. What you may ask for. How you must ask. What you will get back.
  5. It also says, quietly, one more thing: everything not written down is not promised, and may change without warning.
  6. An API is not a program. It is the shape of the doorway into a program.
  7. The most important word in this chapter is contract. An API is a contract between the person who wrote a piece of software and the person who wants to use it.
  8. Because it is a contract and not a technology, an API can be made of almost anything. Text over a wire. A name in a file. A number in a register.
  9. So the sentence “we built an API” tells you nothing about the technology. It only tells you that somebody wrote down a promise.

PLAIN36.1.2 a picture in your head#

  1. Think of a restaurant kitchen. You are hungry. The kitchen can cook.
  2. You do not walk into the kitchen. You are handed a menu.
  3. The menu is the API. It lists exactly what you may ask for. It gives the name of each dish and what comes with it.
  4. You do not need to know whether the soup is made from a family recipe or from a packet. You do not need to know who the cook is today.
  5. The kitchen can throw out its old stove and buy a new one. Your order form does not change. That is the whole benefit in one line.
  6. And if the menu says “soup” but the kitchen sends a sandwich, the kitchen has broken the contract, even if the sandwich is delicious.

Where this comparison breaks:

  1. A menu is a suggestion. An API is checked by a machine, and a machine has no good manners. A single wrong letter in the order and nothing is served.
  2. A restaurant serves one customer at a time at one table. A busy API may be serving fifty thousand callers in the same second, and they can interfere with each other in ways that diners cannot.
  3. A menu never fails halfway. An API call over a network can be taken, cooked, and then lost on the way back, which is a case with no restaurant equivalent. Section 36.5 deals with exactly that case.

PLAIN36.1.3 a worked example#

  1. Here is the same idea, an API, written at five completely different scales. All five are APIs. None of them is more of an API than the others.
Scale The published agreement
One function int strlen(const char *s)
One library curl_easy_setopt(h, opt, v)
The kernel write(fd, buf, count)
A GPU driver vkQueueSubmit(queue, n, ...)
The internet GET /repos/{owner}/{repo}
  1. Scale one is inside one program. The function strlen promises: give me the address of a run of text ending in a zero byte, and I return its length.
  2. Scale two is a library on your machine. The C library libcurl promises: set this option to this value on this handle, and I return zero if I accepted it.
  3. Scale three crosses the boundary into the operating system. The write call promises: I will try to write that many bytes, and tell you how many I wrote.
  4. Scale four crosses into a device driver and then into hardware. Vulkan promises: I will hand this batch of work to the graphics card’s queue.
  5. Scale five crosses the planet. The GitHub HTTP API promises: ask me for that path with a valid token and I will return a JSON description of that repository.
  6. Notice what changed across the five, and what did not. The technology changed completely. The idea did not change at all.
  7. What did change is how much can go wrong. At scale one, essentially nothing. At scale five, everything. That is section 36.5.

PLAIN36.1.4 what is really happening inside#

  1. An API has three parts, and all three matter.
  2. The surface is the list of things you can call, and their names.
  3. The shape of each call is the data you must supply and the data you get back: how many items, of what kind, in what order, in what units.
  4. The behaviour is what the call actually does, including how it fails, how long it may take, and whether calling it twice is different from once.
  5. Beginners think an API is only the surface. Most real bugs live in the behaviour.
  6. The surface can be checked by a machine. A compiler will refuse to build code that calls strlen with two arguments.
  7. The shape can be partly checked by a machine. Types catch some mistakes.
  8. The behaviour usually cannot be checked at all. Nothing stops a function documented as “returns quickly” from taking nine seconds.
  9. So a good API is a small surface, a strict shape, and behaviour written down in plain sentences that a person actually reads.

The honest version: the contract also includes things nobody wrote down.

  1. If your code depends on a list coming back in alphabetical order, and the documentation never promised an order, you have invented a clause.
  2. When the other side changes and your code breaks, both of you will believe you were right. This is the single most common source of API arguments.
  3. The rule that resolves it: if it is not in the document, it is not promised, no matter how long it has been true in practice.

TECHNICAL36.1.5 the engineer’s version#

  1. Formally, an API is an interface specification: a set of named operations, each with a signature, a set of pre-conditions and post-conditions, and an error model.
  2. Two systems that share an interface specification are said to be interoperable at that interface. Neither needs the other’s source.
  3. The specification may be enforced statically (the compiler rejects bad calls), dynamically (the runtime throws), or not at all (documentation only).
  4. The formal notion behind this is design by contract, named by Bertrand Meyer for the Eiffel language in the 1980s: pre-conditions, post-conditions and invariants stated as part of the interface.
  5. An API is distinct from a protocol. A protocol specifies bytes on a wire and the ordering rules between peers. An API specifies operations available to a caller. HTTP is a protocol; the GitHub REST API is an API that uses it.
  6. An API is distinct from an ABI (application binary interface), which fixes register use, calling convention, structure layout and symbol naming. Section 36.3 measures the difference.
  7. Real interface surfaces, for scale. These are order-of-magnitude figures from published documentation and headers, not exact counts, because every one of them depends on version and configuration.
Interface Rough size of surface
C standard library about 200 functions
Linux system calls about 350 on x86-64
Win32 core DLLs thousands of functions
POSIX.1-2024 over 1,200 interfaces
GitHub REST API over 1,000 endpoints
  1. Tools that let you observe an interface directly: nm -D libc.so.6 lists exported symbols, objdump -T shows the dynamic symbol table, man 2 syscalls lists Linux system calls, strace records them as they happen, and curl -v prints an HTTP exchange in full.

WORDS36.1.6 remember these#

  1. API — a published agreement about how to ask software to do something — an interface specification of operations, signatures and error behaviour.
  2. Contract — a promise both sides rely on — the pre-conditions, post-conditions and invariants attached to an interface.
  3. Interface — the doorway into a component — the named boundary across which two components interact without sharing internals.
  4. Protocol — the rules for talking on a wire — the byte format and message ordering between peers, independent of any one caller’s API.
  5. Implementation detail — how it happens to work today — behaviour observable but not specified, therefore free to change without notice.
  6. Interoperable — two things can work together — able to interact correctly through a shared interface specification without shared source code.

36.2 The history, properly#

PLAIN36.2.1 in simple words#

  1. The reader asked how APIs were invented. The honest answer is that nobody invented them in one moment. They were arrived at, in stages, over 75 years.
  2. Stage one, around 1951. People noticed they were writing the same little piece of code again and again, so they saved it and reused it.
  3. That reusable piece was called a subroutine. To reuse it you needed to know what to give it and what it gave back. That written-down knowledge was the first API, before anybody had a name for it.
  4. Stage two, the 1960s. Computers grew operating systems. A program could no longer touch the disk directly. It had to ask the operating system.
  5. That asking needed rules, so operating systems published their own lists of calls. The name application program interface appears in print in 1968.
  6. Stage three, the 1980s. Machines were joined by networks. Somebody asked: why can we not call a piece of code on another machine the same way we call one on this machine?
  7. That idea is the remote procedure call. It was tried properly in 1984, and it worked, and it also created every hard problem in the rest of this chapter.
  8. Stage four, the 1990s. Big vendors built big frameworks to make remote calls easy across languages. They were called CORBA and DCOM. They mostly failed.
  9. Stage five, from 1998 to 2002. People gave up on frameworks and used the web instead: plain text over HTTP, through the same port a browser uses.
  10. Stage six, the 2000s. Companies started publishing these web interfaces to the outside world, so that strangers could build on top of their business.
  11. Stage seven, since 2015. New shapes appeared for particular problems: GraphQL for clients that want to choose their own data, gRPC for fast machine-to-machine traffic inside a company.

PLAIN36.2.2 a picture in your head#

  1. Think of a workshop that makes wooden chairs.
  2. At first one person makes the whole chair. Everything is in their head.
  3. Then somebody makes a jig, a wooden guide for cutting a leg to the right length. Now anyone can cut a leg. That is a subroutine.
  4. Then a drawer of jigs appears, with labels. That is a library.
  5. Then the workshop gets so big that the wood store is locked, and only a storekeeper may enter. You fill in a slip to get wood. That is a system call.
  6. Then a second workshop opens across town. You want to send slips there too. Someone invents a courier. That is a remote procedure call.
  7. And the moment the courier exists, new problems exist: the van breaks down, the slip arrives twice, the other workshop is closed.

Where this comparison breaks:

  1. Jigs do not have versions, and a jig never returns a different result on Tuesday. Software interfaces change under you, which is section 36.3.
  2. A courier who fails tells you he failed. A network can swallow a message and tell nobody, which is the hardest single fact in this chapter.

PLAIN36.2.3 a worked example#

  1. Take one task and watch it move across the eras: “give me the photographs belonging to user 42”.
  2. In 1951, the task does not exist, but the shape does. You would look up a subroutine in a paper catalogue, copy its tape into your program, and know from the catalogue that it wanted a number in one register.
  3. In 1975, on a Unix machine, you would open a file, read it, and close it. Three system calls, all local, all fast, all likely to succeed.
  4. In 1988, on a network, you would call a function named getPhotos(42) that secretly sent a message to another machine. It looked local. It was not.
  5. In 1995, with CORBA, you would write an interface file, run a compiler on it, and get a stub object in your language that hid the network again.
  6. In 1999, with SOAP, you would send a page of XML describing a method call over HTTP, and get back a page of XML describing the answer.
  7. In 2005, with a web API, you would ask for a URL and get a small block of JSON. Here is the whole thing:
GET /users/42/photos HTTP/1.1
Host: api.example.com

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

{"photos":[{"id":9,"title":"Pune, monsoon"}]}
  1. In 2016, with GraphQL, you would send one query naming exactly the fields you wanted, and get back exactly those fields and nothing else.
  2. Look at step 6 against step 7. Same task. The XML version was often two to five kilobytes; the JSON version above is under 60 bytes of payload. That size difference is a large part of why the industry moved.

PLAIN36.2.4 what is really happening inside#

  1. The history is not random. Each step is a response to a specific pain.
  2. Subroutines answered “I am typing the same thing repeatedly”.
  3. Subroutine libraries answered “I cannot find the piece I wrote last year”.
  4. Operating system calls answered “two programs both wrote to the disk and destroyed each other’s data”.
  5. Shared libraries answered “forty programs each carry their own copy of the same code and the machine has 4 MB of memory”.
  6. Remote procedure calls answered “the data I need is on another machine”.
  7. CORBA and DCOM answered “my two programs are in different languages”.
  8. XML over HTTP answered “corporate firewalls block everything except port 80, and I cannot get the network team to open anything”.
  9. REST-style web APIs answered “SOAP is enormous and my phone has a slow connection and a small battery”.
  10. GraphQL answered “my mobile screen needs six fields and the endpoint sends ninety”.
  11. gRPC answered “we make two million internal calls per second and JSON parsing is now a measurable part of our electricity bill”.
  12. Read that list backwards and you can predict the next step: whatever hurts most at scale today is what gets replaced next.

TECHNICAL36.2.5 the engineer’s version#

  1. The timeline, with checkable dates.
Year Event Who
1951 Subroutine library book Wilkes, Wheeler, Gill
1968 Term “application program interface” Cotton, Greatorex
1974 API applied to databases C. J. Date
1981 RPC named in a PhD thesis Bruce Jay Nelson
1984 Working RPC published Birrell and Nelson
1988 POSIX 1003.1 published IEEE
1991 CORBA 1.0, October OMG
1993 Win32 with Windows NT 3.1 Microsoft
1996 DCOM ships Microsoft
1998 XML-RPC published Dave Winer, UserLand
2000 SOAP 1.1 W3C Note, 8 May W3C submission
2000 REST defined in a thesis Roy Fielding
2000 First commercial web API, 7 Feb Salesforce
2000 Web API, 20 November eBay
2002 Amazon Web Services, 16 July Amazon
2004 Photo API Flickr
2006 Platform API, August Facebook
2006 Public API, September Twitter
2012 OAuth 2.0, RFC 6749, October IETF
2015 GraphQL specification opened Facebook
2016 gRPC 1.0, 23 August Google
  1. 1951. Maurice Wilkes, David Wheeler and Stanley Gill published The Preparation of Programs for an Electronic Digital Computer at Cambridge, generally called the first book on programming. It grew out of a September 1950 internal report on the EDSAC subroutine library. The subroutines lived on punched paper tape in a filing cabinet, and the catalogue describing how to call each one is, in every meaningful sense, the first API document.
  2. 1968. The term “application program interface”, without the “-ing”, appears in the paper Data structures and techniques for remote computer graphics by Ira W. Cotton and Frank S. Greatorex, presented at the AFIPS Fall Joint Computer Conference held 9 to 11 December 1968. It described the boundary that gave a graphics program independence from the hardware. This is the commonly cited first published use. It is a first citation, not a moment of invention: the practice was already twenty years old.
  3. 1974. C. J. Date’s paper The Relational and Network Approaches: Comparison of the Application Programming Interface carried the idea into databases, and the “-ing” form became standard.
  4. 1981 and 1984. Bruce Jay Nelson’s doctoral work at Carnegie Mellon named the remote procedure call. Andrew Birrell and Nelson then built one at Xerox PARC, reported in Xerox technical report CSL-83-7 in December 1983 and published as Implementing Remote Procedure Calls in ACM Transactions on Computer Systems, volume 2, number 1, February 1984. It ran on the Cedar system over Ethernet, and reported a null local call at a few microseconds against a null remote call at about 1.1 milliseconds. That ratio is the whole story of distributed systems in one measurement.
  5. 1988. IEEE Std 1003.1-1988 standardized the Unix system call interface. Richard Stallman suggested the name POSIX in place of IEEE-IX.
  6. 1991 to 1997. The Object Management Group, founded 1989, published CORBA 1.0 in October 1991 and CORBA 2.0 with the IIOP wire protocol in 1997. Microsoft shipped DCOM in 1996. Both aimed at language-independent remote objects.
  7. Why they lost, in the analysis of Michi Henning’s 2006 ACM Queue article The Rise and Fall of CORBA: specifications too large and partly never implemented; a committee process driven by vendor politics; no answer to firewalls; and, for DCOM, Windows only. Experts still disagree on the weighting. Henning stresses process failure; Douglas Schmidt’s published reply stresses that the technology worked and the market moved.
  8. There is a second reason, and it is the deeper one. Both tried to make a remote call look exactly like a local call. Section 36.5 explains why that is a lie that cannot be sustained.
  9. 1998 to 2000. Dave Winer of UserLand shipped XML-RPC in 1998, subset from an unshipped 1998 SOAP draft. SOAP 1.1 was published as a W3C Note on 8 May 2000, and SOAP 1.2 became a W3C Recommendation on 24 June 2003.
  10. 2000. Roy Thomas Fielding’s doctoral dissertation Architectural Styles and the Design of Network-based Software Architectures, University of California, Irvine, 2000, supervised by Professor Richard N. Taylor, defined REST in Chapter 5. Fielding was a co-author of the HTTP specifications, so this was a description of the web’s actual architecture, derived, not invented. Section 36.7 covers what it says.
  11. The web API era. Salesforce demonstrated an XML API at the IDG Demo conference on 7 February 2000, widely called the first commercial web API. eBay launched its API and developer programme on 20 November 2000. Amazon launched Amazon Web Services on 16 July 2002. Flickr’s API followed its February 2004 launch. Facebook’s platform arrived in August 2006 and Twitter’s API in September 2006.
  12. 2015 to 2016. GraphQL was built inside Facebook in 2012 for its mobile apps, and the specification and reference implementation were opened during 2015, announced publicly on 14 September 2015. Google built gRPC from its internal Stubby system, announced it in 2015, and released version 1.0 on 23 August 2016.

WORDS36.2.6 remember these#

  1. Subroutine — a saved, reusable piece of code — a callable block with a defined entry point, parameters and return convention.
  2. Subroutine library — a labelled drawer of saved pieces — an indexed collection of reusable routines with published calling documentation.
  3. RPC — calling code on another machine like a local function — remote procedure call, with client stub, marshalling, transport and server skeleton.
  4. Marshalling — packing arguments for travel — serializing typed values into a byte stream and reconstructing them at the far end.
  5. CORBA — a 1990s cross-language remote object standard — Common Object Request Broker Architecture, OMG, CORBA 1.0 in October 1991.
  6. IDL — a file describing an interface once, for many languages — interface definition language, compiled into stubs and skeletons.
  7. SOAP — a heavy XML message format for method calls — W3C Note in May 2000, Recommendation 1.2 in June 2003, usually carried over HTTP.

36.3 Local APIs: signatures, libraries and versions#

PLAIN36.3.1 in simple words#

  1. The simplest API is a function inside a program you are writing.
  2. Its signature is its name, the things it takes, and the thing it gives back. That single line is the whole contract for a lot of code.
  3. When you package many functions together for other people to use, you have a library.
  4. To use a library in C or C++ you need a header file: a file that lists the signatures without the code, so the compiler knows what is allowed.
  5. In other languages the same job is done by import statements, package manifests or type definition files. The idea is identical.
  6. Once other people depend on your library, you are stuck with your own promises. Changing them breaks their programs.
  7. So library authors need a way to say “this change is safe” or “this change will break you”. That way is versioning.
  8. A change that breaks existing callers is called a breaking change. The whole discipline of versioning exists to warn about exactly that.

PLAIN36.3.2 a picture in your head#

  1. Think of a plug and a socket in a wall.
  2. The API is the shape of the plug: three pins, in that arrangement, that size.
  3. Any lamp with that plug shape works. The wall does not care what the lamp is.
  4. Now imagine the electricity company changes the voltage from 230 to 400 while keeping the socket shape identical.
  5. Your plug still fits. Your lamp explodes.
  6. That is the difference between the shape of the interface and its behaviour, and it is exactly how most painful breakages feel.

Where this comparison breaks:

  1. Sockets are regulated by law and change once a century. Libraries change weekly, and any one program may depend on two hundred of them.
  2. A plug either fits or does not. A software interface can half-fit: it compiles, links, runs for six weeks, and then fails on one unusual input.

PLAIN36.3.3 a worked example#

  1. Here is a C function signature and every promise inside it.
size_t fread(void *ptr, size_t size, size_t n, FILE *stream);
  1. fread is the name. That is the only part most people remember.
  2. void *ptr is where to put the data. void * means “any kind of thing”, so the compiler will not check what you point at. That is a hole.
  3. size_t size is how big one item is, in bytes. size_t n is how many.
  4. FILE *stream is the open file to read from.
  5. size_t on the left is the return: the number of complete items actually read, which may be fewer than n. Not bytes. Items.
  6. That last point is a real bug factory. The type says nothing about it. Only the documentation does.
  7. Now the versioning question. Suppose a library goes from 2.4.1 to 3.0.0. Here is exactly what each position promises, under semantic versioning.
Change Example What it promises
Patch 2.4.1 to 2.4.2 Bug fix only, safe
Minor 2.4.1 to 2.5.0 New features, still safe
Major 2.4.1 to 3.0.0 Something broke, read notes
  1. Real cases. Python 2 to Python 3, released December 2008, changed print from a statement to a function and forced a decade-long migration. That is a major change and the number said so.
  2. Adding a new optional argument with a default value is normally a minor change: old callers keep working.
  3. Removing a function, renaming it, changing an argument’s type, or changing what a return value means are all major changes, even when the code still compiles.

PLAIN36.3.4 what is really happening inside#

  1. There are two different boundaries and people confuse them constantly.
  2. The API is what the compiler checks: names, argument counts, types.
  3. The ABI is what the machine checks at run time: which register holds which argument, how big a structure is, where each field sits inside it, what the function symbol is called in the compiled file.
  4. You can keep the API identical and break the ABI. Add one field to the middle of a structure and every already-compiled program that uses that structure now reads the wrong bytes.
  5. Nothing warns you. The program starts. Then it produces nonsense, or dies.
  6. That is why shared libraries carry a separate number in the file name, like libssl.so.3. That number is the ABI version, not the product version.
  7. Rebuilding from source fixes an ABI break. It cannot fix an API break, because the source itself no longer matches.
  8. So the two rules are: change the API and callers must edit their code; change the ABI and callers must at least rebuild.

TECHNICAL36.3.5 the engineer’s version#

  1. Semantic Versioning, written by Tom Preston-Werner, co-founder of GitHub, published as version 1.0.0-beta in 2010 and version 2.0.0 in 2013, states the rule as MAJOR.MINOR.PATCH.
  2. It is a convention, not a standard body’s specification. Package managers such as npm, Cargo and Go modules enforce it mechanically, which makes it feel like a law. Many projects publish semver numbers and break them anyway.
  3. The exact promises from the specification: MAJOR when you make incompatible API changes, MINOR when you add functionality in a backward-compatible way, PATCH when you make backward-compatible bug fixes.
  4. Version 0.y.z means anything may change at any time. That clause is heavily used and heavily abused.
  5. Go modules make major versions part of the import path from v2 onward, so example.com/pkg/v2 and example.com/pkg can coexist in one build. That is a deliberate answer to the diamond dependency problem.
  6. ABI stability in practice, with real numbers:
Interface ABI policy
Linux kernel to userspace Stable, never break it
Linux internal module ABI No stability promise
glibc Stable, symbol versioning
C++ standard library Compiler and flag dependent
Windows Win32 Stable since 1993
  1. glibc uses symbol versioning: one shared object can export memcpy@GLIBC_2.2.5 and a newer variant at once, so old binaries keep the old behaviour. Inspect it with objdump -T /lib/x86_64-linux-gnu/libc.so.6.
  2. Linus Torvalds’s rule for the kernel, stated repeatedly on the kernel mailing list since the 1990s, is that breaking userspace is never acceptable, whatever the technical merit. That policy is why binaries from 1998 still run.
  3. Tools: abidiff from libabigail compares two builds and reports ABI changes; nm -D --defined-only lists exported symbols; readelf -d shows the SONAME that encodes the ABI version; pkg-config --modversion reports the declared version.
  4. Deprecation in code is itself part of the contract: __attribute__ ((deprecated)) in GCC and Clang, @Deprecated in Java, #[deprecated] in Rust, DeprecationWarning in Python. A warning is a promise of a future break.

WORDS36.3.6 remember these#

  1. Signature — the name, inputs and output of a function — the typed interface of a callable, checked by the compiler at the call site.
  2. Header — a file listing signatures without code — a declaration-only translation unit included to satisfy the compiler.
  3. ABI — the machine-level agreement — application binary interface: calling convention, register use, structure layout and symbol naming.
  4. Breaking change — a change that makes working code stop working — any change violating the published compatibility promise for that version position.
  5. Semantic versioning — three numbers that say how risky an upgrade is — MAJOR.MINOR.PATCH per the SemVer 2.0.0 convention of 2013.
  6. Symbol versioning — one library holding old and new behaviour at once — versioned symbol names in ELF so old binaries bind to old semantics.
  7. Deprecation — a warning that something will be removed — a documented period during which an interface still works but is scheduled for removal.

36.4 System call APIs#

PLAIN36.4.1 in simple words#

  1. Chapter 18 covered operating systems. Here is the one-line recap needed now.
  2. A running program is not allowed to touch the disk, the network card or another program’s memory. The operating system owns those.
  3. So the program asks. That request is a system call, and the list of possible requests is the operating system’s API.
  4. This is the most important API on any machine, because everything else is built on top of it.
  5. Different operating systems chose different lists. That is why a program written for one does not simply run on another.
  6. To fix that, people wrote down a common list that many systems agreed to provide. That agreement is called POSIX.
  7. Windows did not follow POSIX. It has its own list, called Win32.
  8. So writing a file looks different on each, and that difference is the reason for a huge amount of the software world’s duplicated effort.

PLAIN36.4.2 a picture in your head#

  1. Think of a bank counter with a thick glass screen.
  2. You cannot reach the money. You write on a slip, push it under the glass, and the clerk does the work.
  3. The list of slips the bank accepts is the system call API. There are maybe 350 kinds of slip on Linux.
  4. POSIX is like an agreement between many banks in many countries to accept the same slips, so a traveller can use any of them.
  5. Win32 is a bank that never signed that agreement and prints its own slips, which are longer and ask for more detail.

Where this comparison breaks:

  1. A bank clerk can ask you what you meant. A kernel cannot. A malformed slip returns a number and nothing else.
  2. A bank does one thing at a time per counter. A kernel handles millions of these per second and must keep every program’s slips separate and safe.

PLAIN36.4.3 a worked example#

  1. Writing the five bytes “hello” to a new file, on both systems, cut to the bone.
/* POSIX */
int fd = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
write(fd, "hello", 5);
close(fd);
/* Win32 */
HANDLE h = CreateFileA("out.txt", GENERIC_WRITE, 0, NULL,
                       CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
DWORD written;
WriteFile(h, "hello", 5, &written, NULL);
CloseHandle(h);
  1. Same task. Same result on disk. Completely different shape.
  2. POSIX gives back a small integer, the file descriptor, usually 3 for your first opened file, since 0, 1 and 2 are already taken by input, output and errors.
  3. Win32 gives back a HANDLE, an opaque value that is not an index into anything you can reason about.
  4. POSIX signals failure by returning -1 and setting a separate global number called errno. Win32 returns a special invalid handle value and you call GetLastError().
  5. POSIX packs the options into one integer of flags. Win32 spreads them across seven separate arguments, several of which are usually zero or NULL.
  6. Notice write returns how many bytes it wrote. It may write fewer than you asked. Ignoring that return value is one of the classic C bugs.

PLAIN36.4.4 what is really happening inside#

  1. A system call is not an ordinary function call. It crosses a privilege boundary, and that is deliberate and expensive.
  2. Your program puts a number identifying the call into a register, puts the arguments into other registers, and executes one special instruction.
  3. On x86-64 Linux that instruction is syscall, and the call number goes in register rax. Write is number 1.
  4. The processor switches from user mode to kernel mode, jumps to a fixed handler, and the kernel checks everything you passed.
  5. The kernel does the work, puts a result back in a register, and returns to user mode.
  6. The C library function write() is a thin wrapper around this. It is not the system call itself; it is a normal function that performs one.
  7. POSIX does not define the instruction, the register or the call number. It defines the C function and its behaviour. That is the portability layer.
  8. So POSIX is an API standard, not an ABI standard: source portability, not binary portability. Code recompiles; compiled binaries do not travel.

TECHNICAL36.4.5 the engineer’s version#

  1. POSIX is IEEE Std 1003.1, first published 1988. The current joint text is maintained by the Austin Group as the Single UNIX Specification and IEEE Std 1003.1; the 2024 edition is the current revision at the time of writing in 2026.
  2. Real conformance, honestly stated: Linux is broadly POSIX-compatible but has never been certified. macOS is certified UNIX. AIX and z/OS are certified. Windows is not POSIX and dropped its old POSIX subsystem long ago.
  3. Windows Subsystem for Linux version 2, shipped from 2019, runs a real Linux kernel in a lightweight virtual machine rather than translating calls. That is why its system call fidelity is high and its file access across the Windows boundary is slow.
  4. Comparison of the two families at the same job:
Task POSIX Win32
Open a file open CreateFileW
Write bytes write WriteFile
Handle type int descriptor HANDLE
Error report errno GetLastError
Start a process fork + exec CreateProcess
Load a library dlopen LoadLibrary
Memory map mmap MapViewOfFile
  1. Cost, measured. A system call on modern x86-64 Linux costs roughly 100 to 300 nanoseconds when nothing goes wrong, against roughly 1 to 2 nanoseconds for a plain function call. Kernel page-table isolation, added in January 2018 against the Meltdown vulnerability, increased that cost measurably on affected processors.
  2. This is why high-throughput code avoids system calls: buffered I/O to batch write calls, io_uring on Linux since kernel 5.1 in 2019 to submit many operations with one crossing, and kernel bypass networking for the extreme cases.
  3. Observation tools: strace -c ./prog counts and times every system call; ltrace does the same for library calls; dtruss is the macOS equivalent; Windows uses Process Monitor and Event Tracing for Windows.
  4. Linux keeps roughly 350 system calls on x86-64, and the list is architecture-specific. ausyscall --dump or the table in arch/x86/entry/syscalls/syscall_64.tbl in the kernel source gives the numbers.

WORDS36.4.6 remember these#

  1. System call — a request from a program to the operating system — a controlled entry into kernel mode with checked arguments.
  2. File descriptor — the small number that stands for an open file — a per process index into the kernel’s open file table, an int on POSIX.
  3. POSIX — the agreed common list of Unix-style calls — IEEE Std 1003.1, first published 1988, source-level portability across conforming systems.
  4. Win32 — the Windows system interface — the C API exported by kernel32, user32, gdi32 and friends, ABI-stable since Windows NT 3.1 in 1993.
  5. User mode and kernel mode — the two privilege levels — CPU rings restricting which instructions and memory a running thread may touch.
  6. Wrapper — the ordinary function that performs the special instruction — the libc stub that loads the call number and executes syscall.

36.5 Remote APIs: what the network changes#

PLAIN36.5.1 in simple words#

  1. This is the single biggest conceptual jump in a developer’s life, and most people make it without noticing, which is why it hurts.
  2. A local function call either happens or the program crashes. There is no third case.
  3. A remote call has a third case, and a fourth, and a fifth.
  4. It can fail before arriving. It can arrive and fail. It can arrive, succeed, and the answer can be lost on the way home.
  5. In that last case, the work was done and you do not know it. You cannot know it. There is no way to find out from the failure itself.
  6. This is called partial failure, and it does not exist inside one machine.
  7. Also, remote calls take real time. A local call costs about a nanosecond. A call across a city costs about ten million nanoseconds.
  8. That is a factor of ten million. No amount of clever coding hides a factor of ten million.
  9. So the rule is: never pretend a remote call is a local call. Every framework that tried to hide the difference eventually failed at exactly this point.

PLAIN36.5.2 a picture in your head#

  1. Asking a colleague sitting beside you for a number is a local call. You ask, they answer, you carry on.
  2. Now the colleague moves to another building and you must send letters.
  3. You post a letter asking for the number. Silence for a week.
  4. Did the letter get lost? Did they get it and not reply yet? Did they reply and that letter got lost? Did they leave the company?
  5. You cannot tell. All four look identical from your desk.
  6. Now suppose the letter asked them to transfer money. Do you send a second letter? If the first one arrived, you have transferred twice.
  7. That is the entire problem, and section 36.10 gives the fix, which is to put a unique reference number on the letter.

Where this comparison breaks:

  1. Letters arrive in days. Network messages arrive in milliseconds, which tempts you into thinking they are instant. They are not; they are just fast.
  2. The post has no equivalent of a message being delivered twice because your own side automatically resent it. Networks do that constantly.

PLAIN36.5.3 a worked example#

  1. The reader’s own session shows this exactly. From a flat in India, curl -v https://github.com printed Trying 20.207.73.82:443... and then timed out after 15 seconds with no response of any kind.
  2. Nothing came back. No refusal, no error message from the network, nothing. Silence.
  3. On the same phone, over mobile data, the same site loaded instantly.
  4. Now ask the question that matters. Was GitHub down? No: the mobile test proves it was serving.
  5. Was the request received? Unknown, and unknowable from the client side.
  6. For a GET that does not matter, because reading a page twice is harmless.
  7. For a POST that creates something, it matters enormously.
  8. In the same session, two git push operations failed with send-pack: unexpected disconnect while reading sideband packet. Both times the push had not been applied, but that could not be known from the error.
  9. The disconnect happened while reading the response. A lost response tells you nothing about whether the write landed. The only way to find out was to ask the server again afterwards.
  10. That is partial failure, observed in a real session, on a real evening.

PLAIN36.5.4 what is really happening inside#

  1. Here is the timeline of one remote call, with the failure windows marked.
client                                            server
  |                                                  |
  |--(1) request leaves -----------------------------|
  |        [lost here -> nothing happened]           |
  |                                                  |
  |                          (2) server does work    |
  |                          [crashes here -> ???]   |
  |                                                  |
  |<-(3) response returns ---------------------------|
  |        [lost here -> IT HAPPENED, you dont know] |
  |                                                  |
  |  (4) timeout fires. Client sees identical error  |
  |      for cases 1, 2 and 3.                       |
  1. Case 1 and case 3 produce the same visible result at the client: a timeout.
  2. In case 1 retrying is correct and free. In case 3 retrying may double the effect. The client cannot tell them apart.
  3. This is not a bug in any particular system. It is a proved limit. The general result is that no protocol can guarantee both sides agree, in the presence of arbitrary message loss.
  4. So the industry does not solve it. It works around it, in three ways.
  5. Make operations idempotent, so that doing them twice equals doing them once. Then retrying is always safe.
  6. Or attach a unique key to each attempt, so the server can recognize a repeat and return the first answer instead of acting again.
  7. Or accept the risk and reconcile later, which is what accounting systems did for centuries before computers.

TECHNICAL36.5.5 the engineer’s version#

  1. The eight fallacies of distributed computing. The list originated at Sun Microsystems: L. Peter Deutsch drafted seven in 1994, drawing on earlier work by Bill Joy and Dave Lyon, and James Gosling added the eighth around 1997. Attribution of individual items is disputed in the sources; the list is not.
# Fallacy What it costs you
1 The network is reliable No retry path
2 Latency is zero Chatty designs
3 Bandwidth is infinite Huge payloads
4 The network is secure Plaintext, no auth
5 Topology does not change Hardcoded addresses
6 There is one administrator No cross-team plan
7 Transport cost is zero Ignored serialization
8 The network is homogeneous Protocol assumptions
  1. Fallacy 1. Packets are dropped, links flap, and TCP connections are reset by middleboxes. Every remote call needs an explicit failure path, not an exception handler bolted on afterwards.
  2. Fallacy 2. Latency has a floor set by physics. Light in fibre travels about 200,000 km per second, so Mumbai to London and back, about 14,400 km of fibre, cannot beat roughly 72 ms of pure propagation, and measures 110 to 130 ms in practice. A design that makes 50 sequential calls has bought itself six seconds before writing a line of logic.
  3. Fallacy 3. Bandwidth is finite and often asymmetric, and mobile uplinks are far slower than downlinks. Payload size is a design decision, not an afterthought.
  4. Fallacy 4. Everything crossing a network is hostile territory. This is why Chapter 32 covered TLS and why section 36.9 covers tokens.
  5. Fallacy 5. Addresses change. Containers get new addresses on every restart, which is why service discovery and DNS-based routing exist.
  6. Fallacy 6. Your call crosses networks owned by people you will never meet. In the reader’s traceroute, the path crossed the home router, the ISP’s private core at addresses like 172.31.0.17, the ISP’s public edge at 137.97.29.249, and then Microsoft’s backbone through Delhi, Mumbai and Pune. That is at least three administrative domains, none of which the reader could telephone.
  7. Fallacy 7. Serializing and deserializing costs CPU and memory. At Google scale this is why Protocol Buffers exist; JSON parsing at millions of requests per second is a real budget line.
  8. Fallacy 8. Different operating systems, TLS versions, HTTP versions, proxies and MTUs. A path that works over one network may silently fail over another, which is precisely what the reader observed between broadband and mobile.
  9. The standard model: at-most-once delivery risks losing work, at-least-once risks duplicating it, and exactly-once delivery is not achievable at the transport level. What is achievable is exactly-once effect, built from at-least-once delivery plus idempotency. Say it that way and you will never be wrong.

WORDS36.5.6 remember these#

  1. Partial failure — the work happened but you never found out — a call whose outcome is unknown to the caller because the response was lost.
  2. Idempotent — doing it twice is the same as doing it once — an operation whose repeated application produces the same state as a single application.
  3. Latency — the waiting time before an answer starts — round-trip time, floored by propagation delay and inflated by queueing and processing.
  4. Timeout — giving up after waiting — a client-side deadline that converts an unknown outcome into a definite local error.
  5. At-least-once — it will arrive, maybe more than once — a delivery guarantee requiring the receiver to tolerate duplicates.
  6. Exactly-once effect — the result happens once even if the message does not — at-least-once delivery combined with server-side deduplication.

36.6 HTTP APIs in practice#

PLAIN36.6.1 in simple words#

  1. Most APIs you meet today are carried over HTTP, the same protocol your browser uses for web pages.
  2. That was not a technical decision at first. It was a political one. Port 443 was the one port that was open everywhere.
  3. An HTTP exchange has exactly two messages: a request you send and a response you get back.
  4. A request has four parts: a method saying what kind of action, a URL saying what to act on, headers giving extra facts, and an optional body carrying data.
  5. A response has three parts: a status code, a three-digit number saying how it went, then headers, then an optional body.
  6. The methods you need are GET to read, POST to create, PUT to replace, PATCH to change part, and DELETE to remove.
  7. The body is almost always JSON these days: text that describes data using braces, square brackets, names and values.
  8. That is the whole thing. Everything else in this section is detail.

PLAIN36.6.2 a picture in your head#

  1. An HTTP request is a filled-in form pushed through a letterbox.
  2. The method is the big word at the top: COLLECT, DEPOSIT, REPLACE, DESTROY.
  3. The URL is the address line: which office, which department, which file.
  4. The headers are the small boxes down the side: who you are, what format you can read, how long you will wait.
  5. The body is the attached sheet, and only some kinds of form have one.
  6. The response comes back with a three-digit stamp at the top. 200 means done. 404 means no such file. 500 means the office caught fire.

Where this comparison breaks:

  1. Paper forms are read by a person who can guess your intent. HTTP is parsed by a machine, and an unexpected header can be silently ignored or fatal.
  2. A form goes to one office. An HTTP request may pass through a browser cache, a company proxy, a content delivery network and a load balancer, any of which may answer it without the real server ever hearing about it.

PLAIN36.6.3 a worked example#

  1. Here is a complete raw exchange, exactly as it goes on the wire, with every part labelled. This is what curl -v shows you.
POST /v1/orders?notify=true HTTP/1.1
Host: api.example.com
Authorization: Bearer sk_live_9f2a...
Content-Type: application/json
Accept: application/json
Content-Length: 52
Idempotency-Key: 8f14e45f-ea4f-4c1b-9b4a-0c1d2e3f4a5b

{"item_id":"sku_771","qty":2,"currency":"INR"}
  1. Line 1 has three things: the method POST, the path /v1/orders, and the query string ?notify=true. Then the protocol version.
  2. Host says which site, because one server address may serve thousands.
  3. Authorization carries the credential. Bearer means “whoever holds this may act”, which is exactly as dangerous as it sounds.
  4. Content-Type says what the body is. Accept says what you want back. These are two different things and mixing them up is a common mistake.
  5. Content-Length is the body size in bytes. Idempotency-Key is a unique reference for this attempt, explained in section 36.10.
  6. A blank line ends the headers. Everything after it is the body.
  7. The response:
HTTP/1.1 201 Created
Content-Type: application/json
Location: /v1/orders/ord_5512
RateLimit-Remaining: 98
Cache-Control: no-store

{"id":"ord_5512","status":"pending","total":1798}
  1. 201 Created says a new thing exists. Location says where it now lives.
  2. RateLimit-Remaining says how many more calls you may make in this window.
  3. Cache-Control: no-store forbids anyone in the middle from keeping a copy.

PLAIN36.6.4 what is really happening inside#

  1. Two properties decide how a method may be treated, and they are not the same property.
  2. Safe means the call is not supposed to change anything. It is a read.
  3. Idempotent means doing it many times leaves the same state as doing it once. Safe methods are automatically idempotent.
  4. GET, HEAD and OPTIONS are safe. PUT and DELETE are idempotent but not safe. POST and PATCH are neither.
  5. Why this matters in practice: browsers, proxies and client libraries are allowed to retry safe and idempotent requests automatically.
  6. So DELETE /orders/5512 sent twice must leave the order deleted, not produce an error the second time. Many APIs get this wrong.
  7. And POST /orders sent twice creates two orders. That is correct behaviour and it is exactly why idempotency keys exist.
Method Safe Idempotent Body
GET Yes Yes No
HEAD Yes Yes No
OPTIONS Yes Yes No
POST No No Yes
PUT No Yes Yes
PATCH No No Yes
DELETE No Yes Rare

TECHNICAL36.6.5 the engineer’s version#

  1. HTTP semantics are now specified in RFC 9110, published June 2022, which replaced the RFC 7230 to 7235 series from 2014, which in turn replaced RFC 2616 from June 1999. Quote RFC 9110 for semantics and RFC 9112 for HTTP/1.1 message syntax.
  2. JSON is specified in RFC 8259, December 2017, with media type application/json. It has no comment syntax, no trailing commas, and its numbers are IEEE 754 doubles in practice, so integers above 2^53 lose precision. Send large identifiers as strings.
  3. The status codes worth knowing, and when to use each:
Code Meaning Use when
200 OK Read or update succeeded
201 Created New resource made
202 Accepted Queued, not done yet
204 No Content Success, nothing to send
301 Moved Permanently URL changed forever
304 Not Modified Client’s cache is valid
400 Bad Request Malformed or invalid input
401 Unauthorized No or bad credential
403 Forbidden Valid identity, not allowed
404 Not Found No such resource
405 Method Not Allowed Wrong verb for that URL
409 Conflict State clash, version clash
410 Gone Deleted on purpose
415 Unsupported Media Type Wrong Content-Type sent
422 Unprocessable Content Syntax fine, semantics bad
429 Too Many Requests Rate limit hit
500 Internal Server Error Server bug
502 Bad Gateway Upstream returned garbage
503 Service Unavailable Overloaded or in maintenance
504 Gateway Timeout Upstream did not answer
  1. The 401 versus 403 distinction is worth memorizing, because it is wrong in most codebases. 401 means “I do not know who you are, authenticate”; the header name WWW-Authenticate is required with it. 403 means “I know exactly who you are and you still may not”.
  2. 429 comes from RFC 6585, April 2012, which also defined 428, 431 and 511. Pair it with Retry-After, which takes either seconds or an HTTP date.
  3. 4xx means the client should change something before retrying. 5xx means the client may retry the identical request later. That single rule drives most retry logic.
  4. Content negotiation uses Accept, Accept-Encoding and Accept-Language in the request against Content-Type, Content-Encoding and Content-Language in the response, with Vary telling caches which of them affected the answer. Omitting Vary is a classic cache poisoning bug.
  5. Conditional requests: the server sends ETag, the client sends If-None-Match on the next read and gets 304 with no body, or sends If-Match on a write and gets 412 if someone else changed it first. That second pattern is optimistic concurrency control and costs almost nothing.
  6. HTTP/2, RFC 9113, multiplexes many requests over one TCP connection and compresses headers with HPACK. HTTP/3, RFC 9114, runs over QUIC on UDP, removing head-of-line blocking at the transport layer. Both change performance dramatically and change the semantics above not at all.

WORDS36.6.6 remember these#

  1. Method — the verb saying what kind of action — the HTTP request method defining semantics, safety and idempotency per RFC 9110.
  2. Status code — the three-digit result — 1xx informational, 2xx success, 3xx redirection, 4xx client error, 5xx server error.
  3. Header — a small labelled fact attached to a message — a field name and value carrying metadata separate from the body.
  4. Safe method — a read that changes nothing — a method whose semantics are defined as read-only, therefore automatically retryable.
  5. ETag — a short label for a version of a thing — an opaque validator used with If-None-Match and If-Match for caching and concurrency.
  6. Content negotiation — asking for the format you can read — matching Accept family request fields against server representations, signalled by Vary.

36.7 REST, properly#

PLAIN36.7.1 in simple words#

  1. REST is the most misused word in this entire subject.
  2. It was defined in 2000 by Roy Fielding in his doctoral thesis. He was describing why the web itself works so well at enormous scale.
  3. He did not describe a technology. He described a set of six rules that an architecture may follow, and what each rule buys you.
  4. The rules are: separate client from server; keep no client state on the server between calls; say clearly what may be cached; allow layers in between; use one uniform way of addressing and acting on things; and, optionally, allow the server to send code the client runs.
  5. Almost nothing sold as REST today follows all six.
  6. Most of what people call REST is really “JSON sent over HTTP with nouns in the URL”. That is a useful style. It is not what the thesis specified.
  7. This matters less than purists claim and more than beginners think. It matters because two of the six rules, statelessness and cacheability, are the reason the web scales, and people abandon them without realizing.

PLAIN36.7.2 a picture in your head#

  1. Think of a public library with open shelves.
  2. Every book has a fixed shelf mark. You can hand that mark to anyone and they find the same book. That is addressability.
  3. The librarian does not remember you between visits. You bring your card each time. That is statelessness, and it means any librarian can serve you.
  4. Some books are marked “reference, always current” and some “loan copy, fine for a month”. That is cacheability.
  5. And each book ends with a list of related shelf marks, so you can keep going without asking anyone. That last one is the rule everybody skips.

Where this comparison breaks:

  1. A library has one building. A REST system may have caches and proxies that answer you without the library ever knowing you asked.

PLAIN36.7.3 a worked example#

  1. Resource-oriented design in one page. The nouns get URLs; the verbs come from HTTP.
GET    /v1/orders                list orders
POST   /v1/orders                create one
GET    /v1/orders/ord_5512       read one
PATCH  /v1/orders/ord_5512       change some fields
DELETE /v1/orders/ord_5512       remove it
GET    /v1/orders/ord_5512/items sub-collection
  1. Plural nouns, no verbs in the path, lower case, hyphens not underscores. All of that is convention, not standard. Nothing enforces it.
  2. Filtering, sorting and paging go in the query string, never in the path: GET /v1/orders?status=paid&sort=-created_at&limit=50.
  3. Now paging, which is where designs break at scale. Two options.
  4. Offset paging: ?limit=50&offset=1000. Simple, and lets you jump to page 21.
  5. Cursor paging: ?limit=50&after=eyJpZCI6Nzcx.... The cursor is an opaque marker for “the last row you saw”.
  6. Cursor wins at scale for two reasons. First, a database must count and skip 1,000 rows to serve offset 1,000, so page 400 is far slower than page 1.
  7. Second, if a new row is inserted while you page, offset paging shows you one row twice and skips another. Cursor paging does not.
  8. Partial responses let the client ask for less: ?fields=id,status,total. That is a convention borrowed from GraphQL’s core idea.

PLAIN36.7.4 what is really happening inside#

  1. The rule that carries the most weight is statelessness.
  2. Stateless means every request carries everything needed to understand it. The server keeps nothing about you between calls.
  3. The payoff is that any server can answer any request. So you can put ten servers behind one address, and add an eleventh at lunchtime.
  4. The moment you store a session in one server’s memory, that request must come back to that server, and your ten servers become ten fragile pets.
  5. The cost of statelessness is that every request repeats its credentials and context, so requests are bigger. That trade was made deliberately.
  6. The rule everyone skips is called HATEOAS: hypermedia as the engine of application state. It means the response tells you what you can do next, by including links, so the client never hardcodes URLs.
  7. The web browser does this perfectly. You do not type URLs; you follow links a server gave you. Almost no JSON API does it.

The honest version: an API without HATEOAS is not REST as Fielding defined it.

  1. Fielding said so directly in a 2008 post on his own site titled “REST APIs must be hypertext-driven”. The industry read it, agreed, and carried on.
  2. Practical position: use the six constraints as a checklist of trade-offs, not a purity test. Know which ones you are breaking and why.

TECHNICAL36.7.5 the engineer’s version#

  1. The six constraints from Chapter 5 of Fielding’s 2000 dissertation, with what each one actually buys:
Constraint What it buys
Client-server Independent evolution
Stateless Horizontal scaling
Cacheable Fewer round trips
Layered system Proxies, CDNs, gateways
Uniform interface Generic tooling
Code on demand Optional, rarely used
  1. Code on demand is the only optional constraint. JavaScript delivered to a browser is the canonical example, and it is why the web can extend clients without redeploying them.
  2. The uniform interface itself decomposes into four sub-constraints: identification of resources, manipulation through representations, self-descriptive messages, and hypermedia as the engine of application state.
  3. A resource in Fielding’s terms is a concept, not a row and not a file. A representation is one rendering of it at one moment, in one media type. The URL identifies the resource; the response body is only a representation.
  4. Richardson’s Maturity Model, presented by Leonard Richardson in 2008, classifies real APIs: level 0 is one URL and one verb, level 1 adds resources, level 2 adds HTTP verbs and status codes, level 3 adds hypermedia. Most commercial APIs sit at level 2. This is a convention for describing maturity, not a standard.
  5. Hypermedia formats that do exist: HAL, JSON:API version 1.1, Siren, and application/problem+json from RFC 9457, July 2023, which replaced RFC 7807 from March 2016 for error bodies.
  6. Cursor paging in production: GitHub uses Link headers with rel="next", an application of RFC 8288 web linking. Stripe uses starting_after with an object identifier. Both are cursor schemes; neither exposes an offset.
  7. Cost of offset paging, measured: a LIMIT 50 OFFSET 1000000 on PostgreSQL must scan and discard a million rows, taking seconds where a keyset query using WHERE id > ? ORDER BY id LIMIT 50 on an indexed column takes sub-millisecond time regardless of depth.
  8. Statelessness has a cost you can measure: repeating a 900-byte JWT on every request costs 900 bytes uplink per call. HTTP/2 HPACK header compression removes most of that repetition on a persistent connection.

WORDS36.7.6 remember these#

  1. REST — a style of network architecture with six rules — Representational State Transfer, defined in Chapter 5 of Fielding’s 2000 dissertation.
  2. Stateless — the server remembers nothing about you between calls — each request carries all context needed for the server to understand it.
  3. Resource — the thing a URL names — a concept with an identifier, distinct from any single representation of it.
  4. Representation — one rendering of a thing at one moment — the bytes plus media type returned for a resource.
  5. HATEOAS — the answer tells you what you can do next — hypermedia as the engine of application state, the constraint almost nobody implements.
  6. Cursor paging — “carry on from this row” — keyset pagination using an opaque marker, stable under insertion and constant-time at any depth.

36.8 The alternatives, compared honestly#

PLAIN36.8.1 in simple words#

  1. REST-style JSON over HTTP is the default, not the only choice. Four alternatives matter.
  2. GraphQL gives one endpoint and lets the client write a query saying exactly which fields it wants. The server returns exactly that shape.
  3. gRPC sends compact binary messages defined in advance by a schema file. It is fast and strictly typed, and it can stream in both directions.
  4. SOAP is the older XML approach. It is verbose and heavy, and it is still running in banking, insurance, telecoms and government.
  5. JSON-RPC is the minimal option: one endpoint, a method name, parameters, an identifier. About two pages of specification in total.
  6. And for pushing data to a client, WebSockets give a two-way permanent connection, while server-sent events give a simpler one-way stream from server to client.
  7. None of these replaces the others. Each solves a specific pain.

PLAIN36.8.2 a picture in your head#

  1. Ordering food. REST is a menu with fixed dishes; you order dish 12 and get what dish 12 contains, including the garnish you did not want.
  2. GraphQL is telling the kitchen your exact ingredients, and getting a plate with only those. Wonderful for you, much harder for the kitchen to plan.
  3. gRPC is a factory canteen with pre-printed cards and numbered slots. Fast, rigid, and useless to a visitor who does not have the card printer.
  4. WebSockets are leaving the intercom switched on both ways all evening.

Where this comparison breaks:

  1. A kitchen can refuse a silly order. A GraphQL server can be handed a query that legally asks for a million rows through four levels of nesting, and will try, unless somebody wrote a cost limit.

PLAIN36.8.3 a worked example#

  1. The same request, three ways. First GraphQL:
POST /graphql
{"query":"{ user(id:42){ name photos(first:2){ title } } }"}

{"data":{"user":{"name":"Asha","photos":[
  {"title":"Pune, monsoon"},{"title":"Bhor ghat"}]}}}
  1. One call, one round trip, exactly the fields asked for. With REST this often needs two calls: one for the user, one for the photos.
  2. That is the over-fetching and under-fetching problem GraphQL solves, and on a mobile connection it is worth real seconds.
  3. Now the same thing as a gRPC schema, written in Protocol Buffers:
message PhotoRequest { int64 user_id = 1; int32 first = 2; }
message Photo { string title = 1; int64 taken_at = 2; }
service Photos {
  rpc List(PhotoRequest) returns (stream Photo);
}
  1. The numbers 1 and 2 are field tags. They go on the wire instead of the field names, which is most of why the encoding is small.
  2. stream Photo means the server can send photos one at a time as it finds them, rather than making the client wait for all of them.
  3. And JSON-RPC, for contrast, is this and nothing more:
{"jsonrpc":"2.0","method":"listPhotos",
 "params":{"user":42},"id":7}

PLAIN36.8.4 what is really happening inside#

  1. Every one of these has a cost, and the honest version of each is the part the marketing leaves out.
  2. GraphQL solves over-fetching and creates two new problems.
  3. Problem one is caching. HTTP caching works on URLs and methods. GraphQL sends everything as a POST to one URL, so every proxy, CDN and browser cache in the world becomes useless to it.
  4. Problem two is the N+1 query problem. Asking for 100 users and each user’s photos naively becomes 1 query for users plus 100 for photos. The standard fix is a batching loader that gathers the 100 into one query, but you must build it. The API shape hides the database cost from the caller.
  5. gRPC is fast and typed, and it is rare in browsers, for a concrete reason: it needs control over HTTP/2 frames and trailers that browser JavaScript cannot access. The workaround, gRPC-Web, needs a proxy to translate, so it is extra machinery.
  6. SOAP is heavy but it brought things REST had to reinvent: a machine-readable contract in WSDL, and standards for signing and encrypting individual message parts rather than the whole channel.
  7. WebSockets give you a persistent two-way channel, and in exchange you now own reconnection, heartbeats, backpressure and message ordering yourself.
  8. Server-sent events do far less and cost far less: it is plain HTTP, it reconnects automatically, and the browser gives you an event identifier so you can resume. It only goes one way, which is what most apps need.

TECHNICAL36.8.5 the engineer’s version#

  1. Comparison on the axes that decide real choices:
Aspect REST/JSON GraphQL gRPC
Payload Text, medium Text, tuned Binary, small
Browser Native Native Needs proxy
Typing Optional Schema, strong Schema, strong
Streaming SSE or WS Subscriptions Native, 2-way
HTTP caching Excellent Poor Not applicable
Tooling Universal Good, newer Good, generated
  1. Encoding sizes, illustrative, for one small record of five fields: SOAP envelope roughly 1,200 bytes, JSON roughly 150 bytes, Protocol Buffers roughly 40 to 60 bytes. Exact figures depend entirely on field names and values, so measure your own payloads rather than quoting these.
  2. Protocol Buffers achieve that by sending field tag numbers instead of names, using variable-length integers, and omitting fields at default values. The cost is that the message is unreadable without the schema.
  3. gRPC supports four call shapes: unary, server streaming, client streaming and bidirectional streaming. It runs over HTTP/2, uses trailers for status, and carries a status code set of its own, where OK is 0 and DEADLINE_EXCEEDED is 4.
  4. GraphQL’s specification is maintained by the GraphQL Foundation, hosted by the Linux Foundation since 2018. Practical defences you must add yourself: query depth limits, query cost analysis, persisted queries, and a batching loader such as DataLoader.
  5. SOAP lives on where contracts and message-level security matter: SWIFT and ISO 20022 messaging in banking, many telecom provisioning systems, and government tax and customs filing. It is not a legacy curiosity; it is running production traffic in 2026.
  6. JSON-RPC 2.0, specified in 2010, is used by Ethereum nodes, the Language Server Protocol behind editor tooling, and the Model Context Protocol used for tool calling by AI systems. Small specifications survive.
  7. Push mechanisms: WebSockets are RFC 6455, December 2011, upgrading an HTTP connection to a two-way frame protocol. Server-sent events are part of the HTML living standard, media type text/event-stream, one way, with automatic reconnection and a Last-Event-ID header for resumption. Long polling still exists and still works where nothing else gets through.
  8. Selection rule, stated plainly. Public API for unknown clients: REST. Many different client shapes over one graph of data: GraphQL. Internal service-to-service at high volume: gRPC. Live updates to a browser, one way: server-sent events. Live two-way, such as a game or a chat: WebSockets.

WORDS36.8.6 remember these#

  1. Over-fetching — getting more fields than you needed — a fixed representation returning data the client discards, wasting bandwidth and battery.
  2. N+1 problem — one query becomes a hundred — resolving a list then issuing one dependent query per element instead of one batched query.
  3. Protocol Buffers — a compact binary format with a schema file — Google’s IDL and wire format using numbered field tags and varint encoding.
  4. Streaming — sending results as they appear — a call that yields many messages over one open call instead of one response.
  5. WebSocket — a permanent two-way channel — RFC 6455 framing over an upgraded HTTP connection, full duplex, no request-response structure.
  6. Server-sent events — a one-way live feed over plain HTTP — the text/event-stream media type with automatic client reconnection.

36.9 Authentication and authorization for APIs#

PLAIN36.9.1 in simple words#

  1. Two different questions, and confusing them causes real breaches.
  2. Authentication asks: who are you? Authorization asks: are you allowed to do this particular thing?
  3. An API key is a long random string you send with each request. It answers neither question well. It identifies the caller’s account and nothing else.
  4. Basic auth sends a username and password on every request, lightly encoded, not encrypted. Only safe inside TLS, and even then it is blunt.
  5. A bearer token is a string that means “whoever holds this may act”. It is the standard today. It usually expires, which is its main improvement.
  6. OAuth 2.0 is the system that lets you grant one app limited access to your account on another service, without giving it your password.
  7. A scope is a label on a token limiting what it may do. Not who you are. What this token may do.
  8. A JWT is a token with readable information inside it and a signature proving nobody edited it.
  9. mTLS is where both sides present certificates, so the connection itself proves identity before any request is sent.

PLAIN36.9.2 a picture in your head#

  1. Think of a hotel.
  2. Your passport at the desk is authentication. It proves who you are, once.
  3. The room card you get is a bearer token. It expires on checkout day, and anyone who finds it can open your room. It does not know your name.
  4. The card opens room 402 and the gym, but not the wine cellar. Those are scopes.
  5. Handing your passport to a tour company so they can collect your parcel is the bad old way. OAuth is instead telling the desk “issue a card that opens only the parcel locker, valid for one hour”.
  6. And mTLS is the hotel checking your face against a photo while you check the hotel’s licence on the wall. Both sides prove identity.

Where this comparison breaks:

  1. A hotel card is physical, so only one person can hold it. A leaked token can be copied a million times in a second, and the copies are indistinguishable.

PLAIN36.9.3 a worked example#

  1. The OAuth 2.0 authorization code flow with PKCE, step by step. This is the flow you should use for web apps, mobile apps and command-line tools alike.
 user      app                 auth server        API
  |         |                       |              |
  |         |-1 make verifier+chal->|              |
  |<-2 open browser to /authorize---|              |
  |-3 log in and approve ---------->|              |
  |         |<-4 redirect with code-|              |
  |         |-5 POST code+verifier->|              |
  |         |<-6 access + refresh --|              |
  |         |-7 Bearer access token --------------->|
  |         |<-8 data or 403 if scope missing ------|
  1. Step 1. The app invents a random secret called the code verifier, then hashes it to make a code challenge. Real values, computed with SHA-256:
verifier : AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8
challenge: 6oZqdX5MOLq_qBJ8vppAnT4fk6AP8UiP9zX8-Rev_9A
  1. Step 2. The app opens the user’s browser at the authorization server, sending its client identifier, the redirect address, the scopes it wants, a random state value, and the challenge. Never the verifier.
  2. Step 3. The user logs in at the authorization server, not at the app. The app never sees the password. That is the entire point of OAuth.
  3. Step 4. The server redirects back to the app with a short-lived authorization code, plus the same state so the app can detect forgery.
  4. Step 5. The app sends the code and the original verifier to the token endpoint, over a direct connection, not through the browser.
  5. Step 6. The server hashes the verifier, compares it with the challenge from step 1, and only then issues an access token and a refresh token.
  6. Why PKCE matters: if an attacker steals the code in step 4, they still cannot use it, because they do not have the verifier. Without PKCE, a stolen code is a stolen account.
  7. Step 7 and 8. The app calls the API with Authorization: Bearer .... If the token lacks the needed scope, the answer is 403, not 401.

PLAIN36.9.4 what is really happening inside#

  1. A JWT is three base64url-encoded parts joined by dots: header, payload, signature. Here is a real one, signed with HMAC-SHA256 and the secret kedbyte-demo-secret. The middle part is one line, wrapped here to fit the page.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJpc3MiOiJrZWRieXRlIiwic3ViIjoidV80MiIsInNjb3BlIjoi
cmVwbyIsImlhdCI6MTc2NzIyNTYwMCwiZXhwIjoxNzY3MjI5MjAwfQ
.
mgYCqjUldLiGQs_GeAH1CvayV7pxsCKFLswilq9iX8o
  1. Decoding part one gives {"alg":"HS256","typ":"JWT"}. alg is the signing algorithm. typ says this is a JWT.
  2. Decoding part two gives the claims:
Claim Value Meaning
iss kedbyte Who issued it
sub u_42 Who it is about
scope repo What it may do
iat 1767225600 Issued 1 Jan 2026 00:00 UTC
exp 1767229200 Expires one hour later
  1. Part three is the signature: HMAC-SHA256 over the text header.payload, using the shared secret, base64url encoded.
  2. Change one character anywhere in parts one or two and the signature no longer matches. That is the whole security model.
  3. Now the critical point, and it catches people. Base64 is not encryption. Anyone holding the token can read every claim. Put nothing secret in a JWT payload.
  4. The signature proves the token was not altered and came from someone with the key. It does not hide anything.
  5. And what a scope actually restricts: it restricts the token, not the user. The user may have every permission in the world. If the token was issued with scope read:user and you attempt a write, the write is refused.
  6. The reader’s own session showed exactly this. A git push was refused because the personal access token lacked the workflow scope, and the push modified a CI workflow file. The account had permission. The token did not. Chapter 42 walks through that exact refusal end to end.

TECHNICAL36.9.5 the engineer’s version#

  1. Specifications, with dates: OAuth 2.0 is RFC 6749, October 2012. Bearer token usage is RFC 6750, October 2012. PKCE is RFC 7636, September 2015. JWT is RFC 7519, May 2015. JWS signatures are RFC 7515. Token introspection is RFC 7662. Token revocation is RFC 7009.
  2. Current guidance is RFC 9700, Best Current Practice for OAuth 2.0 Security, published January 2025. It requires PKCE for all client types, requires exact redirect URI matching, and rules out the implicit grant and the resource owner password credentials grant.
  3. That is a change with a date, so state it as one: advice that was correct in 2015, such as “use the implicit flow for single-page apps”, is now explicitly wrong.
  4. Credential mechanisms compared:
Mechanism Expires Scoped Main weakness
API key Rarely Sometimes Leaks live forever
Basic auth No No Password on every call
Bearer token Yes Yes Theft equals full use
JWT Yes Yes Hard to revoke early
mTLS Cert lifetime Via cert fields Certificate management
  1. Why API keys are weak, precisely: they are long-lived, usually coarse-grained, frequently committed to source control, and often sent in query strings where they land in server access logs and browser history. GitHub’s secret scanning finds millions of leaked credentials a year.
  2. JWT signature algorithms: HS256 uses one shared secret, so every verifier can also mint tokens. RS256 and ES256 use a private key to sign and a public key to verify, so verifiers cannot mint. Prefer asymmetric for anything crossing a trust boundary.
  3. The classic JWT vulnerability is accepting alg: none, or accepting an attacker-chosen algorithm. Always pin the expected algorithm in the verifier rather than reading it from the token you are verifying.
  4. Access tokens should be short-lived, commonly 5 to 60 minutes. Refresh tokens are long-lived and must be stored securely; RFC 9700 requires that refresh tokens for public clients be sender-constrained or rotated on each use, so that a stolen one is detectable when both copies are presented.
  5. mTLS, mutual TLS, means the client presents an X.509 certificate during the handshake. It is standard inside service meshes such as Istio and Linkerd, where certificates are issued automatically and rotated hourly. RFC 8705 defines binding OAuth tokens to a client certificate, which stops a stolen token being used from another machine.
  6. Observation: curl -v shows the Authorization header being sent, jwt command line tools or any base64 decoder reveal claims, and the token introspection endpoint of RFC 7662 tells you server-side whether a token is still live.

WORDS36.9.6 remember these#

  1. Authentication — proving who you are — verifying an identity claim against a credential the verifier trusts.
  2. Authorization — deciding what you may do — evaluating a policy against an authenticated identity and a requested operation.
  3. Bearer token — whoever holds it may act — a credential requiring no proof of possession beyond presentation, per RFC 6750.
  4. Scope — a limit written into the token — a named restriction on the operations a specific token may perform, independent of the user’s rights.
  5. JWT — a signed, readable token — JSON Web Token, RFC 7519: base64url header, claims and signature, integrity-protected but not encrypted.
  6. PKCE — proof that the app finishing the flow is the app that started it — RFC 7636 code verifier and S256 challenge, now required for all clients.
  7. Refresh token — the long-lived key used to get new short-lived ones — a credential exchanged at the token endpoint, rotated or sender-constrained.
  8. mTLS — both sides show certificates — mutual TLS authentication binding identity to the transport connection itself.

36.10 Making an API reliable in the real world#

PLAIN36.10.1 in simple words#

  1. An API that works when everything is calm is easy. An API that works when things go wrong is the whole job.
  2. Six habits do almost all of the work. Learn these six and you are ahead of most working code.
  3. Rate limiting is the server saying “you may only ask me this many times in this much time”. It protects the server from being flattened.
  4. Retrying is the client saying “that failed, let me try again”. Done badly, it turns a small problem into an outage.
  5. Backoff means waiting longer before each retry. Jitter means adding a random amount to that wait so that everyone does not retry together.
  6. An idempotency key is a unique reference you attach to a request, so the server can recognize a repeat and refuse to do the work twice.
  7. A timeout is a deadline. Without one, a slow answer becomes a stuck program, and stuck programs pile up until the machine dies.
  8. A circuit breaker stops calling a service that is clearly broken, so you fail fast instead of queueing up thousands of doomed calls.
  9. Graceful degradation means giving a worse but useful answer instead of no answer: a slightly old price, a shorter list, a page without the map.
  10. Now the one rule to carry out of this section, stated plainly. A client that retries a request which is not idempotent, without an idempotency key, can charge a real person twice for the same purchase.
  11. That is not a rare edge case. It is the normal outcome of a lost response plus an automatic retry, and it happens on ordinary evenings.

PLAIN36.10.2 a picture in your head#

  1. Picture two buckets, side by side. They are the two standard ways to limit a rate, and they behave differently in a way that matters.
  2. The first bucket holds tokens. A tap drips a new token in at a steady speed. The bucket has a maximum size and overflows if it fills.
  3. To make a request you must take a token out. No token, no request.
  4. If you have been quiet for a while, the bucket is full, and you may fire off a whole burst at once. That is the token bucket.
  5. The second bucket has a small hole in the bottom. You pour requests in at the top. They drip out of the hole at one fixed speed, no matter how fast you pour.
  6. If you pour too fast the bucket fills up and the rest spills on the floor. That is the leaky bucket.
  7. So the token bucket forgives a burst after a quiet period. The leaky bucket never forgives a burst; it smooths everything into one steady stream.
  8. Now the circuit breaker, which is a different picture. It is the fuse in the fuse box of a house.
  9. When a wire keeps short-circuiting, the fuse blows and cuts the power. It does not keep pushing current into a fault until the house catches fire.
  10. Later, somebody carefully switches it back on to see whether the fault has been fixed. If it fails again, it trips again straight away.

Where this comparison breaks:

  1. Real buckets and taps are continuous. Software counts in discrete steps and checks the clock, so a token bucket is arithmetic, not plumbing.
  2. A house fuse is reset by a person. A circuit breaker in software resets itself on a timer, which means it can flip back and forth if you choose the timer badly.
  3. Water in a bucket is anonymous. Rate limits are almost always counted per caller, so there are millions of tiny buckets, not one big one.

PLAIN36.10.3 a worked example#

  1. First, the maths of a herd, because this is the part people do not believe until they see the numbers.
  2. Set the scene. Ten thousand clients are talking to one service. The service restarts and is unavailable for 30 seconds.
  3. Every client fails at roughly the same instant, because they all failed for the same reason: the service went away.
  4. Each client uses plain exponential backoff with a base of one second: wait 1, then 2, then 4, then 8, then 16, then 32 seconds.
  5. Because they all started together, they all wake together. Here is when each retry wave lands, measured from the moment of failure.
Attempt No jitter, all arrive at Full jitter, spread over
1 t = 1 s 0 to 1 s
2 t = 3 s 0 to 2 s
3 t = 7 s 0 to 4 s
4 t = 15 s 0 to 8 s
5 t = 31 s 0 to 16 s
6 t = 63 s 0 to 32 s
  1. Without jitter, all ten thousand requests arrive inside the same few milliseconds. Suppose the service can handle 500 requests per second.
  2. Ten thousand requests in, say, 10 milliseconds is an instantaneous rate of one million requests per second. That is 2,000 times capacity.
  3. So the wave fails. And because it fails together, the next wave is still synchronized. The herd never breaks up. This is the thundering herd.
  4. Worse: the service comes back at t = 30 s, gets hit by the whole herd at t = 31 s, falls over again, and the outage extends itself.
  5. Now add jitter. The rule called full jitter is: wait a random amount between zero and the current backoff window.
  6. At attempt 6 the window is 32 seconds wide. Ten thousand clients spread evenly over 32 seconds is 10,000 divided by 32, which is about 312 requests per second.
  7. 312 is comfortably under the 500 the service can take. The herd has become a queue, and the service recovers.
  8. That is the entire argument for jitter, in one division. Randomness turns a spike into a flow, and it costs one call to a random number generator.
  9. Second worked example: the double charge, step by step, with real money.
t=0.0s  client POSTs /charges  amount 4999  (49.99 in minor units)
t=0.4s  server creates charge ch_A, moves 4999, starts reply
t=0.4s  the reply is lost somewhere on the path back
t=5.0s  client timeout fires. Client sees: "no response"
t=5.0s  client retry logic: "safe to retry, it probably failed"
t=5.4s  server creates charge ch_B, moves 4999 AGAIN
        customer has now paid 99.98 for one order
  1. Nothing here is a bug in the usual sense. Every component did what it was told. The client could not tell a lost request from a lost response.
  2. Now the same story with an idempotency key. The client generates a random identifier once, before the first attempt, and reuses it on every retry.
t=0.0s  POST /charges  Idempotency-Key: 8f14e45f-ea4f-4c1b-9b4a-0c1d
t=0.4s  server records key 8f14... -> charge ch_A, result 201
t=0.4s  reply lost
t=5.0s  client retries with the SAME key 8f14...
t=5.4s  server finds the key already recorded
t=5.4s  server replays the stored result. No second charge.
t=5.4s  client receives 201 and the SAME charge id ch_A
  1. Read the last line again. The client gets the original answer, not a new one, and not an error. From the client’s view the call simply succeeded.
  2. The key must be generated once per logical operation, not once per attempt. A new key on each retry recreates the original bug exactly.

PLAIN36.10.4 what is really happening inside#

  1. A token bucket in code is three numbers per caller: how many tokens are left, the refill rate, and the time it was last touched.
  2. Nothing runs on a timer. When a request arrives, the server works out how much time has passed and adds that many tokens, capped at the bucket size.
  3. Worked with numbers. Bucket size 100 tokens, refill 10 tokens per second. The caller has been quiet for a minute, so the bucket is full at 100.
Event Tokens before Tokens after
Burst of 100 requests 100 0
Request 101, same instant 0 refused, 429
100 ms later, 1 request 1 0
Steady state tops up 10/s allows 10/s
  1. So the shape is: a burst of up to 100, then a sustained 10 per second forever. That is usually what you want from a public API.
  2. A leaky bucket used as a queue is different. Requests go into a queue of depth 100 and are released at exactly 10 per second.
  3. Send 100 at once and none are refused, but the hundredth is not served for ten seconds. The burst is paid for in waiting instead of in refusals.
  4. Compare the two directly:
Property Token bucket Leaky bucket
Bursts Allowed up to depth Smoothed away
Output shape Bursty, then steady Constant rate
Excess traffic Refused immediately Queued, then dropped
Adds delay No Yes, up to depth/rate
  1. Which to choose: token bucket when callers are humans or apps that come and go, leaky bucket when you must protect something downstream that cannot take a spike at all, such as a payment processor or a legacy database.
  2. When the limit is hit, the server answers 429 Too Many Requests, and should include a Retry-After header saying how long to wait.
  3. Retry-After may be a number of seconds, such as Retry-After: 30, or an HTTP date. A client that ignores it and hammers on is the reason services start banning integrations.
  4. Now the circuit breaker. It is a small state machine with three states, kept per downstream service.
                failures cross the threshold
   +---------------------------------------------+
   |                                             v
[CLOSED]                                      [OPEN]
   ^  all calls pass through          all calls fail instantly
   |                                             |
   |  probe succeeded                            | cooldown expires
   |                                             v
   +--------------------------------------  [HALF-OPEN]
                                        one probe call allowed
                                        probe failed -> OPEN again
  1. Closed is normal. Calls go through and failures are counted.
  2. Open means the breaker has tripped. Calls are not attempted at all. They fail immediately, locally, in microseconds, with no network involved.
  3. That instant local failure is the point. It frees the threads, sockets and memory that would otherwise be stuck waiting on a dead service.
  4. Half-open is the careful test. After a cooldown, one call is let through. If it works, close the breaker. If not, open it again and wait longer.
  5. Timeouts sit under all of this. There is not one timeout; there is a timeout at every layer, and if any layer has none, the whole chain can hang.
  6. And graceful degradation is the plan for when the breaker is open. You always need an answer for the question: what do we show the user now?

TECHNICAL36.10.5 the engineer’s version#

  1. Status codes and headers, with sources. 429 Too Many Requests comes from RFC 6585, April 2012, and is carried forward in RFC 9110, June 2022. Retry-After is specified in RFC 9110 section 10.2.3 and takes either delta-seconds or an HTTP-date.
  2. Use 429 for “you exceeded your quota” and 503 Service Unavailable for “I am overloaded or in maintenance”. Both may carry Retry-After. This distinction is a convention that most large APIs follow, not a rule the specification enforces.
  3. Standard rate-limit headers are, as of August 2026, still not an RFC. The IETF HTTPAPI working group document draft-ietf-httpapi-ratelimit-headers reached revision 11 in May 2026 and defines two structured fields, RateLimit and RateLimit-Policy, replacing the earlier triple RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset.
  4. In practice, today, most services send their own X-RateLimit-* headers. Treat header names as per-service configuration, not as a standard, and read the documentation for each API you call.
  5. GitHub’s published REST limits, which are a good calibration point:
Caller Requests per hour
Unauthenticated 60
Personal access token 5,000
GitHub App installation 5,000 to 12,500
Enterprise Cloud app 15,000
  1. GitHub returns x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-used, x-ratelimit-reset (UTC epoch seconds) and x-ratelimit-resource, and answers with 403 or 429 when you exceed a limit. GitHub also enforces undocumented secondary rate limits on bursts and on expensive operations, which is why blind parallelism gets integrations blocked.
  2. Backoff formulas. The reference treatment is Marc Brooker’s AWS Architecture Blog post “Exponential Backoff And Jitter”, 4 March 2015. Its three variants, exactly as published:
no jitter    : sleep = min(cap, base * 2 ** attempt)
full jitter  : sleep = random(0, min(cap, base * 2 ** attempt))
equal jitter : temp  = min(cap, base * 2 ** attempt)
               sleep = temp/2 + random(0, temp/2)
decorrelated : sleep = min(cap, random(base, sleep * 3))
  1. The post’s simulations found full jitter best overall on total work done and competitive on completion time. Use full jitter unless you have measured a reason not to. Typical parameters: base 100 ms, cap 20 s, 5 attempts.
  2. Only retry what is retryable. Retry on connection errors, timeouts, 429, 502, 503 and 504. Do not retry 400, 401, 403, 404, 409 or 422; the answer will not change and you are just generating load.
  3. Idempotency keys. Idempotency-Key is described in the IETF draft draft-ietf-httpapi-idempotency-key-header, revision 07, October 2025. It is still a draft, not an RFC, but the header name is a de facto industry convention.
  4. Stripe’s implementation is the reference behaviour, and its documented rules are worth copying exactly:
Rule Stripe’s behaviour
Key form V4 UUID suggested, 255 chars max
Storage Status code and body saved
Retention Pruned after at least 24 hours
Different params, same key Request errors
  1. Note the fourth row. Reusing a key with different parameters is an error, not a silent overwrite. That catches the bug where a client reuses a key for a different purchase.
  2. Stripe accepts idempotency keys on all POST requests, and states plainly that they have no effect on GET and DELETE because those are already idempotent by definition per RFC 9110.
  3. Server-side, the usual implementation is a table keyed on (account, idempotency key) with a unique index, holding a state of in-progress or complete plus the serialized response, written in the same database transaction as the effect.
  4. Timeouts, layer by layer. Every one of these has a default, and several common defaults are effectively infinite.
Layer Setting Sensible value
DNS lookup resolver timeout 2 to 5 s
TCP connect connect timeout 2 to 5 s
TLS handshake handshake timeout 5 s
Response read read timeout 10 to 30 s
Whole call overall deadline 30 s
  1. With curl these are --connect-timeout and --max-time, plus --retry, --retry-delay, --retry-max-time and --retry-all-errors. In Go they are fields on http.Transport plus a context deadline. In Python’s requests a bare timeout= covers connect and read, and omitting it means wait forever, which is the single most common production mistake in that library.
  2. Deadlines should propagate. gRPC does this properly: a deadline set by the caller travels in the grpc-timeout header and is reduced at each hop, so a service that is already out of time does not start new work. HTTP has no standard equivalent, which is why hand-rolled chains leak time.
  3. Hard platform limits exist and will surprise you. Amazon API Gateway had a fixed 29-second integration timeout for years; in June 2024 AWS allowed that limit to be raised beyond 29 seconds for regional REST APIs, subject to an account-level quota increase. Check the number for your platform rather than assuming.
  4. Circuit breakers. The pattern was named and popularized by Michael Nygard in “Release It!”, 2007. Netflix’s Hystrix was the best-known Java implementation; its last release, 1.5.18, was in November 2018, after which Netflix put it into maintenance mode and pointed new work at resilience4j. Envoy, Istio and Linkerd implement the same idea in the proxy layer, outside your code.
  5. Practical breaker settings: trip on a rolling error rate rather than a raw count, for example 50 percent failures over a 10-second window with a minimum of 20 requests; cooldown 5 to 30 seconds; one probe in half-open.
  6. Related and often confused: a bulkhead limits how many concurrent calls one dependency may consume, so a slow dependency cannot eat the whole thread pool. Breakers stop bad calls; bulkheads contain them.
  7. Load shedding is the server-side twin of the breaker. Under overload, reject early and cheaply with 429 or 503 rather than accepting work you cannot finish. Accepting everything and timing out is the worst option because you pay the cost and deliver nothing.
  8. Graceful degradation in concrete terms: serve a stale cached value and mark it stale, drop optional page sections, fall back to a smaller model or a simpler ranking, queue writes for later, or return partial results with a flag saying what is missing.
  9. HTTP even has a header for the stale case: Cache-Control: stale-if-error from RFC 5861, May 2010, lets a cache serve a stale response when the origin errors.
  10. Observation and testing: curl -w prints timing breakdowns such as time_connect and time_starttransfer; ab, wrk, hey and k6 generate load; tc netem and Toxiproxy inject latency and loss; and chaos engineering, as practised by Netflix’s Chaos Monkey from 2011, tests these paths on purpose rather than by accident.

WORDS36.10.6 remember these#

  1. Rate limit — a cap on how often you may ask — a server-enforced quota per caller per window, signalled with 429 and Retry-After.
  2. Token bucket — a jar of tickets that refills steadily — a limiter holding capacity C refilled at rate R, permitting bursts up to C.
  3. Leaky bucket — a bucket with a hole, output always steady — a limiter that queues arrivals and releases them at a fixed rate, smoothing bursts.
  4. Exponential backoff — wait longer after each failure — retry delay growing as base times two to the power of the attempt number, capped.
  5. Jitter — randomness added to the wait — a randomized delay that de-synchronizes retrying clients and prevents a thundering herd.
  6. Thundering herd — everybody retries at the same instant — synchronized client retries producing a load spike far above steady-state capacity.
  7. Idempotency key — a reference number on the request — a client-generated unique value letting the server deduplicate retries and replay the result.
  8. Circuit breaker — a fuse for a failing dependency — a three-state machine, closed, open and half-open, that fails fast while a service is unhealthy.
  9. Bulkhead — a limit on how much one dependency may consume — a bounded concurrency pool per downstream, containing the blast radius of slowness.
  10. Graceful degradation — a worse answer beats no answer — deliberately reduced functionality preserving the core path during partial failure.

36.11 Webhooks: the inversion where the server calls you#

PLAIN36.11.1 in simple words#

  1. Everything so far had the same shape. You call the server. The server answers. You are the one who starts the conversation.
  2. A webhook turns that around. You give the other service a web address of yours, and when something happens, it calls you.
  3. So your program stops being only a client. For webhooks it must also be a server, sitting there with a door open, waiting.
  4. The name comes from “web” plus “hook”, where a hook is an old programming word for a place you can attach your own code so it runs when an event occurs.
  5. The reason webhooks exist is simple: without them you have to keep asking “has it happened yet? has it happened yet?”, which is called polling.
  6. Polling wastes almost all of its calls, and it is always a little late, because you only find out at the next check.
  7. There is a price for the inversion. Anyone on the internet can send a POST to your address, so you must prove that a message really came from the sender you expect.
  8. You do that with a signature: the sender computes a code from the message and a shared secret, and you compute the same code and compare.
  9. And because the network can lose things, senders resend. So the same event can arrive twice, or three times, or out of order. Your handler must cope.
  10. Finally, the practical wall every beginner hits: a laptop at home has no address the internet can reach, so nothing can call you at all. Section 36.11.4 explains why and section 36.11.5 gives the fixes.

PLAIN36.11.2 a picture in your head#

  1. Polling is standing at the window every minute, all day, watching for the postman.
  2. You look 480 times during the working day. The post arrives once. You looked 479 times for nothing, and you found out up to a minute late.
  3. A webhook is a doorbell. You install it once, you give the wire an address, and you get on with your life. It rings when there is something to know.
  4. The signature is the courier saying a password you agreed in advance. Anyone can press a doorbell. Only the real courier knows the word.
  5. The password is not simply spoken, though. It is mixed with the parcel itself, so the word only matches if the parcel is unchanged.
  6. And the courier is stubborn. If nobody answers, they come back in a minute, then in five, then in an hour. So the same parcel can be delivered twice if you took it and then failed to sign for it.

Where this comparison breaks:

  1. A doorbell rings in your house, where you already are. A webhook has to reach a machine that in most cases has no public address at all. That is the difference the whole of section 36.11.4 is about.
  2. A courier delivers parcels in the order they were posted, near enough. A webhook sender makes no such promise, and several large ones explicitly say so in their documentation.
  3. A doorbell that rings twice is obvious to a human. Two identical webhook deliveries look identical to a program, which is why you need event identifiers.

PLAIN36.11.3 a worked example#

  1. Compare the two designs with numbers. A shop gets 5 orders a day and wants to react when a payment succeeds.
  2. Polling every 60 seconds: 60 times 24, which is 1,440 requests a day. Five of them find something. 1,435 are wasted, and news is up to 60 seconds old.
  3. Webhooks: 5 deliveries a day, each arriving about a second after the event. That is 288 times fewer requests and 60 times fresher.
  4. Polling every second to fix the delay makes it 86,400 requests a day for the same 5 pieces of news, and burns your rate limit for nothing.
  5. Now the delivery itself. Here is a real signed webhook, in the style Stripe uses. The values below were computed for this book and are genuine.
POST /hooks/payments HTTP/1.1
Host: shop.example
Content-Type: application/json
Stripe-Signature: t=1767225600,
 v1=9b76fe28839a1536d4ba22b9641546bd14ad4da938f8fa3cfa3f
    e34d35633eb8
Content-Length: 57

{"id":"evt_9f2","type":"payment.succeeded","amount":4999}
  1. The signature line is one line in reality; it is wrapped here to fit the page. t is the time the sender signed it. v1 names the scheme.
  2. To verify, you build the signed string by joining the timestamp, a full stop, and the exact body bytes:
1767225600.{"id":"evt_9f2","type":"payment.succeeded",
"amount":4999}
  1. Then you compute HMAC-SHA256 over that string using the endpoint secret, here whsec_kedbyte_demo, and compare with the v1 value. They match.
  2. Now watch what happens if your web framework parses the JSON and your code re-serializes it before checking. Same data, spaces after the colons:
body as sent : {"id":"evt_9f2","type":"payment.succe...
signature    : 9b76fe28839a1536d4ba22b9641546bd14ad4d...

body re-made : {"id": "evt_9f2", "type": "payment.suc...
signature    : bd01c7e20696abf97e590d6b0e86cae26157bf...
  1. Every character of the signature is different. Nothing about the meaning changed. Four space characters changed the bytes, and HMAC signs bytes.
  2. This is the single most common webhook bug in the world, and now you will never write it: verify against the raw body, before any parsing.
  3. Last piece: the timestamp is inside the signed string. So an attacker who captures a valid delivery and replays it tomorrow cannot change t without breaking the signature, and your tolerance check rejects the old one.

PLAIN36.11.4 what is really happening inside#

  1. A webhook delivery is nothing exotic. It is an ordinary HTTP POST, made by the sender’s servers, to a URL you registered with them earlier.
  2. The sequence, end to end:
 your app      payment service        your receiver
    |                 |                     |
    |-1 register URL->|                     |
    |                 |                     |
    |             2 event happens           |
    |                 |-3 POST + signature->|
    |                 |                4 verify HMAC
    |                 |                5 dedupe on id
    |                 |<-6 200 OK, fast ----|
    |                 |                7 do work async
    |                 |                     |
    |                 |-8 retry if no 200 ->|
  1. Step 4 must come before anything else. An unverified webhook is an instruction from a stranger, and people have been robbed by acting on one.
  2. Step 5 exists because delivery is at-least-once. Every serious sender attaches a unique event identifier, and your job is to remember the ones you have already handled and ignore repeats.
  3. Step 6 must be fast. Senders time out. If your handler does ten seconds of database work before replying, you will be marked as failed and resent the same event, which multiplies your load exactly when you are slow.
  4. So the pattern is: verify, deduplicate, write the event to a queue or table, return 200, and do the real work afterwards in the background.
  5. Ordering. Events are generated in one order and delivered in whatever order the network and the retry queue produce. Stripe states in its documentation that it does not guarantee delivery order.
  6. The fix for ordering is not to sort them. It is to stop depending on order: treat each event as a hint that something changed, and read the current state back from the API before acting on anything important.
  7. Now the wall. Why can a webhook not reach your laptop?
  8. Your machine at home has a private address, something like 192.168.0.14, handed out by your router. Chapter 24 covered these: the ranges 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16 from RFC 1918 are not routable on the public internet.
  9. Your router performs NAT, network address translation. When you make an outbound connection it writes down a mapping and rewrites the addresses, so replies find their way back to you.
  10. The mapping is created by your outbound packet. Nothing creates a mapping for an inbound connection from a stranger. So a POST from a payment company arrives at your router with no idea where to send it, and is dropped.
  11. The reader’s own network makes this worse in a way worth seeing. Their traceroute showed the ISP’s own core using private addresses, 172.31.0.17 and 172.26.22.235 among them, before reaching a public address at 137.97.29.249.
  12. That pattern is carrier-grade NAT: the reader’s home router is itself behind another layer of translation run by the ISP. Even opening a port on the home router would not help, because the public address is not theirs to control.
  13. So the answer is not to accept an inbound connection at all. The answer is to make an outbound connection from inside, which NAT allows, and let the inbound requests travel back down it. That is a tunnel, and Chapter 34 covered exactly this mechanism for VPNs and proxies.

TECHNICAL36.11.5 the engineer’s version#

  1. History and standardization. Jeff Lindsay coined the term “web hook” in a blog post on 3 May 2007. There was no specification for twenty years. On 13 December 2023 the Standard Webhooks project published an open specification, with a steering committee drawn from Zapier, Twilio, Lob, Mux, ngrok, Supabase and Kong. It is a convention with momentum, not an RFC.
  2. Header conventions in the wild differ, and there is no winner:
Sender Signature header Algorithm
GitHub X-Hub-Signature-256 HMAC-SHA256, hex
Stripe Stripe-Signature HMAC-SHA256, hex
Standard Webhooks webhook-signature HMAC-SHA256, base64
  1. GitHub signs the raw body alone and prefixes the digest with sha256=. Stripe signs timestamp + "." + raw body. Standard Webhooks signs id + "." + timestamp + "." + raw body. Read the sender’s documentation; guessing the signed string is the second most common webhook bug.
  2. GitHub also sends X-GitHub-Event naming the event type, X-GitHub-Delivery carrying a GUID that identifies the delivery, and X-GitHub-Hook-ID identifying the webhook itself.
  3. Verification checklist, in order:
    1. Read the raw request body as bytes. Disable any middleware that parses, re-encodes, decompresses or pretty-prints it first.
    2. Recompute the HMAC with the endpoint secret.
    3. Compare with a constant-time comparison, such as hmac.compare_digest in Python or crypto.timingSafeEqual in Node, never with ==.
    4. Check the timestamp is recent. Stripe’s libraries default to a 5-minute tolerance and warn against setting it to zero, which disables the check.
    5. Only then parse the JSON.
  4. Constant-time comparison matters because a naive byte-by-byte comparison returns faster on an early mismatch, and that timing difference is enough to recover a signature one byte at a time over many attempts.
  5. Secret rotation: senders usually support two active secrets during a change window. Stripe allows a rolled secret’s predecessor to stay valid for up to 24 hours and sends one signature per active secret in the same header.
  6. Retry behaviour from the sender’s side, with real published policies. Stripe retries deliveries for up to three days in live mode with exponential backoff, and allows manual resends for up to 15 days from the Dashboard and 30 days from the CLI. Other senders differ enormously; some do not retry automatically at all and only offer manual redelivery. Never assume.
  7. Because retries exist, deduplicate on the sender’s event identifier. Stripe recommends recording processed event IDs, and warns that in some cases two distinct Event objects are generated for the same underlying change, so deduplicating on data.object identity plus event.type is sometimes needed as well.
  8. Idempotent handlers are the real defence. Design the handler so that applying the same event twice leaves the same state. Then duplicates are free rather than dangerous, and you are back to the exactly-once effect described in section 36.5.
  9. Return 2xx immediately, then process asynchronously. Stripe treats any 3xx as a failure, so a webhook URL that redirects, for example from a bare domain to www, silently fails every delivery.
  10. Additional hardening: allowlist the sender’s published IP ranges as a second layer; require HTTPS with TLS 1.2 or 1.3; exempt the webhook route from CSRF protection, which otherwise rejects every legitimate delivery; and never trust amounts or ownership from the payload alone for anything financial, since the correct move is to re-read the object from the API.
  11. Receiving on a machine behind NAT. The options, honestly compared:
Approach Works behind CGNAT Cost of setup
Public server or function Yes Deploy something
Router port forward + DDNS No Router config
Reverse tunnel service Yes One command
Cloud queue relay Yes Extra service
  1. Port forwarding fails under carrier-grade NAT for the reason Chapter 24 gave: the public address belongs to the ISP and is shared, so there is no port on it that is yours to map.
  2. Reverse tunnels work because the connection is established outbound from inside the network, which NAT permits, and inbound requests are then multiplexed back down that already-open connection.
  3. The common tools: ngrok, first released by Alan Shreve in 2013 and incorporated in 2015; Cloudflare Tunnel, launched as Argo Tunnel in 2018 and made free for everyone on 15 April 2021 with the cloudflared daemon; Tailscale Funnel; and plain ssh -R to any host you already own, which needs GatewayPorts enabled on that host.
  4. Vendor CLIs do the same thing without a public URL at all. stripe listen --forward-to localhost:4242/webhook opens an outbound connection to Stripe and replays events to your local port, printing a signing secret for the session. gh webhook forward does the equivalent for GitHub.
  5. Testing tools: stripe trigger payment_intent.succeeded fires a real test event; request-bin style services capture and display raw deliveries; and the sender’s own delivery log, which shows the exact request and your exact response, is almost always the fastest way to find the fault.
  6. Webhooks versus the alternatives, in one table:
Mechanism Direction Best for
Polling Client asks Simple, low event rate
Webhook Server calls you Rare events, any client
WebSocket Both, held open Live two-way, chat
Server-sent events Server pushes One-way live feed
Message queue Broker in middle High volume, ordering
  1. At scale, webhooks become a queue problem, and teams move to a broker such as Amazon EventBridge, Google Pub/Sub or Kafka. Stripe supports sending events directly to EventBridge or Azure Event Grid instead of to an HTTP endpoint, which removes your receiver from the critical path entirely.

WORDS36.11.6 remember these#

  1. Webhook — the other service calls you when something happens — a user-registered HTTP callback delivering event notifications by POST.
  2. Polling — asking repeatedly whether anything changed — client-initiated periodic requests, costly and bounded in freshness by the interval.
  3. HMAC — a code proving a message came from someone with the secret — keyed-hash message authentication code, RFC 2104, usually with SHA-256.
  4. Raw body — the exact bytes that arrived — the unparsed request payload, the only correct input to signature verification.
  5. Replay attack — resending a captured valid message — defended by signing a timestamp and rejecting deliveries outside a tolerance window.
  6. Delivery identifier — the sender’s unique name for this event — an event or delivery ID used by the receiver to detect and drop duplicates.
  7. Reverse tunnel — an outbound connection used to carry inbound requests — a NAT-traversal technique multiplexing public traffic to a private host.
  8. Carrier-grade NAT — the ISP translates too, not just your router — RFC 6598 shared address space, making inbound port mapping impossible for the user.

36.12 Designing an API people can actually use#

PLAIN36.12.1 in simple words#

  1. A usable API is mostly boring. It is boring in the same way, everywhere, and that sameness is the feature.
  2. Naming. Use nouns for things and let the method say the verb. A path called /orders/8812 with a DELETE is better than /deleteOrder?id=8812.
  3. Consistency. If one part of your API uses created_at and another uses creationTime, every user of your API will get it wrong forever.
  4. Errors with codes. Send back a short machine-readable code such as insufficient_funds, alongside the human sentence.
  5. That code matters far more than the sentence, and here is why: sentences get reworded, translated and improved, and every rewording silently breaks any program that was matching on the text.
  6. A code is part of the contract. A message is not. If you promise nothing about the message, you may fix its typos without breaking anyone.
  7. Versioning. There are four honest answers to “how do we change things without breaking people”, and each costs something. Section 36.12.5 compares them without pretending one is obviously right.
  8. Deprecation. Say a thing is going away, say when, say what to use instead, and then actually keep to the date.
  9. Documentation. Reference pages tell you what exists. Guides tell you how to do the common job. You need both, and you need examples that run.
  10. A machine-readable description. If your API is described in a file a computer can read, then documentation, client libraries, test doubles and input checking can all be generated from that one file.
  11. A sandbox. Somewhere users can try everything with fake money and fake data, and get predictable answers, without fear.

PLAIN36.12.2 a picture in your head#

  1. Think of a large hardware shop with many aisles.
  2. A good shop puts screws in one place, and every box is labelled the same way: size, then length, then material, then quantity.
  3. Once you have read one label you can read all of them. You never have to learn the shop twice.
  4. A bad shop labels some boxes by length first and some by material first, and keeps a few screws in the paint aisle for historical reasons.
  5. The stock is identical in both shops. One is pleasant, one is exhausting. That difference is entirely consistency, and it is the same for APIs.
  6. Now the error codes. Imagine the shop’s till printing “Card refused” on one receipt and “Your card has been declined” on another for the same fault.
  7. A person shrugs. A machine that was told to look for the exact words “Card refused” now silently does the wrong thing.
  8. Put a number on the receipt as well, always the same number for the same fault, and the machine can be right regardless of the wording.

Where this comparison breaks:

  1. A shop can rearrange the aisles overnight and customers adapt in a week. An API cannot, because its customers are programs that do not adapt at all.
  2. A shop has one physical layout. An API often has to serve the old layout and the new layout at the same time, for years. That is versioning, and shops have no equivalent of it.

PLAIN36.12.3 a worked example#

  1. Here is one endpoint designed badly, then the same endpoint designed well. Nothing about the underlying system changes.
BAD
GET /getOrderData?orderid=8812&fmt=json&full=1
200 OK
{"ok":0,"err":"Order not found or you dont have access"}

GOOD
GET /v1/orders/ord_8812
404 Not Found
Content-Type: application/problem+json
{"type":"/errors/not-found","code":"order_not_found", ...}
  1. Five faults in the bad version, one at a time.
  2. The path contains a verb, getOrderData, which duplicates what GET already says, and it will sit awkwardly beside updateOrderData later.
  3. The identifier is in a query string, so /getOrderData with no arguments is a different thing again, and caches and logs treat them inconsistently.
  4. The status is 200, meaning success, for a failure. Every client library, proxy and monitoring dashboard in the world will record this as fine.
  5. The error is a sentence with a spelling mistake. Fix the spelling and you break anybody who matched on the string. So the mistake becomes permanent.
  6. The message merges two different situations, “no such order” and “not yours”, so the caller cannot tell them apart even if it wanted to.
  7. Now the good version in full. This is what the body should look like, using the standard problem format:
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{
  "type": "/errors/insufficient-funds",
  "title": "Insufficient funds",
  "status": 422,
  "detail": "Balance 1200 is below the amount 4999.",
  "instance": "/orders/ord_8812",
  "code": "insufficient_funds",
  "balance_minor": 1200,
  "required_minor": 4999
}
  1. code is the stable machine-readable name. title and detail are for humans and may be reworded at any time. instance says which thing failed.
  2. The extra fields balance_minor and required_minor let a client build a useful message itself, in its own language, without parsing English.
  3. Notice the money. It is an integer in minor units, 4999 meaning 49.99, not a decimal number. Section 36.12.5 explains why floats and money do not mix.
  4. Now the same discipline applied to field naming across a whole API:
Decision Pick one Never mix
Field case snake_case with camelCase
Timestamps RFC 3339 UTC with epoch seconds
Identifiers prefixed strings with bare integers
Money integer minor units with decimals
Lists always paginated with sometimes-all
  1. Prefixed identifiers such as ord_8812 and cus_41a are a small idea with a large payoff: a support engineer can tell what a value is by looking at it, and a client cannot accidentally pass a customer where an order goes.

PLAIN36.12.4 what is really happening inside#

  1. Versioning exists because of one asymmetry. You can deploy a change in a minute. Your callers may take three years to update, and some never will.
  2. So the real question is never “what is the tidiest version scheme”. It is “how long am I willing to run the old behaviour, and who pays for it”.
  3. There are four families of answer, and a fifth position that says do not version at all.
  4. Put the version in the path, as /v1/orders. Simple, visible, easy to route in a proxy, easy to explain. The cost is that the version is attached to every URL, so identifiers are not stable across versions and a client that upgrades must rewrite every path it holds.
  5. Put it in a header, as API-Version: 2 or Stripe-Version: 2026-07-29. URLs stay stable and the change is invisible in logs and links, which is both the benefit and the drawback: it is easy to forget you set it.
  6. Put it in the media type, as Accept: application/vnd.kedbyte.v2+json. This is what content negotiation in HTTP was actually designed for, and it is the most correct answer. It is also the one nobody finds friendly, and it breaks casual testing in a browser.
  7. Use dates instead of numbers, so each account is pinned to the behaviour of the day it joined. Very kind to callers, very expensive to run, because the server keeps every old shape alive behind translation layers.
  8. Or do not version at all. Only make additive changes: new fields may appear, nothing is ever removed or given a new meaning. This is what GraphQL encourages and what many internal APIs do. It works until the day you genuinely need to remove something, and then it does not.
  9. Whichever you choose, the deprecation mechanics are the same. Announce with a date. Send a header on every response to the old thing. Publish what to use instead. Then remove it on the day you said.
  10. A useful middle step is the brownout: on announced days before the removal, turn the old endpoint off for an hour, so that anybody still calling it notices while there is still time.
  11. Now the machine-readable description. A file such as an OpenAPI document lists every path, every parameter, every response shape and every error.
  12. Once that file exists, tools can read it and produce: reference documentation, client libraries in a dozen languages, a mock server that returns example data, request validation at the gateway, and a diff between two versions that tells you whether a change is breaking.
  13. The important consequence is subtle. The specification stops being a document that describes the code and starts being a thing the code is checked against. Drift becomes a test failure instead of a support ticket.

TECHNICAL36.12.5 the engineer’s version#

  1. Error format. RFC 9457, Problem Details for HTTP APIs, published July 2023, obsoletes RFC 7807 from March 2016. Media type application/problem+json. Members: type (a URI reference identifying the problem type), title, status, detail, instance, plus any extension members you add.
  2. RFC 9457 is explicit that detail is for humans and that clients should key off type. Add your own short code extension if you want a compact stable token; that is a convention, not part of the RFC.
  3. Do not put secrets, stack traces or internal hostnames in detail. Error bodies are logged, screenshotted and pasted into public issue trackers.
  4. Versioning strategies, compared honestly:
Strategy Example Main cost
URL path /v1/orders URLs churn on upgrade
Header API-Version: 2 Invisible, easy to forget
Media type vnd.x.v2+json Correct but unfriendly
Date-pinned 2026-07-29 Server keeps every shape
None, additive no marker Cannot ever remove
  1. Real practice, with dates. Stripe pins each account to a dated version and names major releases; as of this writing the current version string is 2026-07-29.dahlia, sent in the Stripe-Version header, with monthly releases guaranteed backward compatible and named releases such as Acacia and Basil marking the breaking ones. GitHub uses a dated X-GitHub-Api-Version header. Kubernetes uses group-and-version in the path, such as apps/v1, with an explicit alpha, beta, stable ladder.
  2. Experts genuinely disagree here. Roy Fielding has argued publicly that versioning a REST API in the URL is an admission that hypermedia was not used and that clients were coupled to URL structure. The pragmatic camp replies that /v1/ is legible, greppable and routable, and that shipping beats purity. Both are right about different things.
  3. What counts as a breaking change is the part teams fail to write down. A workable rule: adding an optional field, adding a new endpoint, and adding a new enum value to an output are non-breaking. Removing or renaming a field, adding a required input, narrowing a type, changing a status code, and changing the meaning of an existing value are breaking.
  4. That third one bites. Adding a new enum value is only safe if you told clients in advance to tolerate unknown values. Say it in the documentation on day one, because you cannot add the rule retroactively.
  5. Deprecation headers. RFC 9745, The Deprecation HTTP Response Header Field, published 17 March 2025, defines Deprecation as a structured field date. RFC 8594, May 2019, defines Sunset as the date the resource stops responding. RFC 9745 requires that Sunset not be earlier than Deprecation.
HTTP/1.1 200 OK
Deprecation: @1780272000
Sunset: Wed, 30 Sep 2026 23:59:59 GMT
Link: </docs/migrate-v2>; rel="deprecation"; type="text/html"
  1. The @ form is the structured-field syntax for a Unix timestamp; here it is 1 June 2026. Link with rel="deprecation" points at the migration guide, using web linking from RFC 8288, October 2017.
  2. A humane deprecation policy in five parts: a minimum notice period, stated publicly, of 6 to 12 months for a paid public API; response headers from day one of the notice; direct email to accounts still calling it; a migration guide with before-and-after examples; and scheduled brownouts.
  3. OpenAPI. History: Swagger began in 2010, written by Tony Tam at Wordnik; SmartBear donated the specification to the OpenAPI Initiative under the Linux Foundation in November 2015; it was renamed the OpenAPI Specification on 1 January 2016. Version 3.0.0 was released 26 July 2017, 3.1.0 on 18 February 2021, and 3.2.0 on 23 September 2025. Version 3.1 matters most, because it made the schema language a proper superset of JSON Schema 2020-12.
  4. A minimal but real OpenAPI document:
openapi: 3.1.0
info:
  title: KedByte Orders API
  version: 1.4.0
paths:
  /orders/{orderId}:
    get:
      operationId: getOrder
      summary: Fetch one order
      parameters:
        - name: orderId
          in: path
          required: true
          schema: { type: string, pattern: "^ord_[a-z0-9]+$" }
      responses:
        "200":
          description: The order
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"
        "404":
          description: No such order
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
components:
  schemas:
    Order:
      type: object
      required: [id, status, amount_minor, currency]
      properties:
        id: { type: string }
        status:
          type: string
          enum: [pending, paid, shipped, cancelled]
        amount_minor: { type: integer, minimum: 0 }
        currency: { type: string, minLength: 3, maxLength: 3 }
        created_at: { type: string, format: date-time }
  1. What that file buys you, concretely: swagger-ui and redoc render documentation; openapi-generator and oapi-codegen emit clients and server stubs; prism runs a mock server; schemathesis and dredd test the live API against the document; oasdiff reports breaking changes between two revisions; and gateways including Kong, Envoy and Amazon API Gateway can validate requests before they reach your code.
  2. Equivalents in other ecosystems: .proto files for gRPC, the GraphQL SDL for GraphQL, WSDL for SOAP, and AsyncAPI for event-driven and webhook interfaces. OpenAPI 3.1 also gained a top-level webhooks section, so outbound callbacks can be described in the same document.
  3. Generated SDKs, honestly. They give type safety, retries, pagination helpers, authentication and release-note churn for free, and they are how Stripe and Twilio made their APIs pleasant. The costs are real: generated code is often unidiomatic, one bad generator template produces the same bug in twelve languages at once, and every extra language is an extra thing to support forever. Many teams generate a thin core and hand-write the ergonomic layer on top.
  4. Sandboxes. The requirements that matter: separate credentials that cannot touch production, deterministic magic values that force specific outcomes, no real money or messages, and the same version and error behaviour as production. Stripe’s test card numbers are the canonical example, where a specific card number always produces a specific decline code.
  5. Money and time, since they cause more API bugs than anything else. Money: integer minor units plus an ISO 4217 currency code, never a binary floating-point number, because 0.1 plus 0.2 is not 0.3 in IEEE 754. Time: RFC 3339 timestamps in UTC with an explicit offset, and a separate IANA time zone identifier such as Asia/Kolkata when local meaning matters. Never a bare local time.
  6. Documentation that works has four layers: a five-minute quickstart that produces a real result, task guides for the common jobs, generated reference for every field, and a changelog with dates. The quickstart is the one that decides whether a developer stays.

WORDS36.12.6 remember these#

  1. Error code — a short stable name for a failure — a machine-readable token that is part of the contract, unlike the human-readable message.
  2. Problem Details — the standard shape for an error body — RFC 9457, media type application/problem+json, with type, title and status.
  3. Breaking change — existing callers stop working — any change that invalidates a promise the previous version made, whatever the version number.
  4. Deprecation — announced, still working — RFC 9745 Deprecation header marking a resource as scheduled for removal, with a migration path.
  5. Sunset — the date it stops answering — RFC 8594 Sunset header giving the time after which a resource becomes unresponsive.
  6. Brownout — a rehearsal for the switch-off — a deliberate short outage of a deprecated endpoint to surface remaining callers before removal.
  7. OpenAPI — a description of the API a machine can read — the OpenAPI Specification, 3.1.0 from 2021 and 3.2.0 from 2025, formerly Swagger.
  8. SDK — a ready-made client library — a generated or hand-written package wrapping transport, authentication, retries and pagination for one language.
  9. Sandbox — a safe copy to experiment in — an isolated environment with test credentials, deterministic fixtures and no real-world side effects.

36.13 Consuming an API well#

PLAIN36.13.1 in simple words#

  1. Most of your working life you will be the caller, not the builder. Being a good caller is a skill, and it is mostly discipline.
  2. Read the documentation first. Not the tutorial. Find four things: the base address, how to authenticate, the rate limits, and how lists are paged.
  3. Try it by hand before writing code. One curl command tells you more than an hour of guessing, because it shows you the real bytes.
  4. Check the status code. The most common bug in code that calls APIs is assuming it worked and reading a field out of an error body.
  5. Respect the rate limit. The service is telling you, in a header, how much you have left. Read it. Slow down before you are refused.
  6. Do not fetch what has not changed. If the server gave you a label for the version you already have, send it back, and it will tell you “no change” with an empty body.
  7. Page properly. Lists are not lists. They are the first page of a list, and there is a documented way to ask for the next one. Use it, in a loop, with a stopping condition.
  8. Never put a secret in anything the user can see. A key in a web page, a phone app, or a browser extension is a public key, no matter what you named the variable.
  9. That last one is not a style preference. It is the reason a large number of real breaches happened, and the fix is always the same: keep the secret on a server you control and let the user’s device talk to that.

PLAIN36.13.2 a picture in your head#

  1. Calling somebody else’s API is like being a guest researcher in a large library that belongs to another institution.
  2. You get a reader’s card. It is yours, it has your name on it, and lending it to a friend gets you both banned. That is your token.
  3. The library allows twenty book requests an hour. A sign at the desk tells you how many you have left today. That sign is the rate-limit header.
  4. Books come from the stacks a trolley at a time, twenty per trolley, with a slip saying “more available”. That slip is the pagination link.
  5. If you already copied a chapter, the librarian will happily tell you “that chapter has not been revised since you last saw it” instead of fetching the whole volume again. That is the conditional request.
  6. And when the librarian says no, they say why: no such book, not allowed, closed for lunch, or you have asked too often. Four different answers that need four different reactions from you.

Where this comparison breaks:

  1. A librarian notices a rude guest and speaks to them. An API notices nothing and simply blocks your account, often without warning and often at 3 a.m.
  2. A human researcher naturally slows down when tired. A for loop does not, so the politeness must be written into the code deliberately.

PLAIN36.13.3 a worked example#

  1. A full session against a real public API, using the GitHub REST API. The header names and limits below are exactly as GitHub documents them.
  2. Step one, the plain call, with -i so we see the response headers as well as the body:
curl -i \
  -H "Accept: application/vnd.github+json" \
  -H "X-GitHub-Api-Version: 2022-11-28" \
  https://api.github.com/repos/kedbyte/book
  1. Step two, read the top of the answer before reading any of the body:
HTTP/2 200
content-type: application/json; charset=utf-8
etag: W/"7f2c1a9b6e0d4c8f1a2b3c4d5e6f7a8b"
x-ratelimit-limit: 60
x-ratelimit-remaining: 57
x-ratelimit-used: 3
x-ratelimit-reset: 1767229200
x-ratelimit-resource: core
  1. Four facts, immediately. The call worked. There is an etag label for this version. We have 57 of 60 calls left this hour. The window resets at the epoch second 1767229200, which is 1 January 2026 at 01:00 UTC.
  2. The limit is 60 because we sent no token. With a personal access token the same headers would say 5,000. That single header is why anonymous scripts fail at scale and authenticated ones do not.
  3. Step three, ask again politely, sending the label back:
curl -i -H "If-None-Match: \
W/\"7f2c1a9b6e0d4c8f1a2b3c4d5e6f7a8b\"" \
  https://api.github.com/repos/kedbyte/book
  1. If nothing changed, the answer is:
HTTP/2 304
etag: W/"7f2c1a9b6e0d4c8f1a2b3c4d5e6f7a8b"
x-ratelimit-remaining: 57
  1. Read the last line carefully. x-ratelimit-remaining did not go down. GitHub documents that a conditional request returning 304 does not count against your primary rate limit when the request was correctly authorized.
  2. So conditional requests are not merely a bandwidth saving. On this API they are free calls. A poller that ignores ETags burns its quota on data it already has.
  3. Step four, handling the failures rather than assuming success. Here is the shape every calling script should have:
code=$(curl -s -o body.json -w '%{http_code}' \
  --connect-timeout 5 --max-time 30 \
  -H "Authorization: Bearer $GH_TOKEN" \
  https://api.github.com/repos/kedbyte/book)

case "$code" in
  200) jq -r '.full_name, .stargazers_count' body.json ;;
  304) echo "unchanged, using cached copy" ;;
  401) echo "token missing or invalid"; exit 1 ;;
  403|429) echo "rate limited; see reset header"; exit 2 ;;
  404) echo "no such repo, or token cannot see it"; exit 3 ;;
  5*)  echo "server side; retry with backoff" ; exit 4 ;;
  *)   echo "unexpected status $code"; exit 5 ;;
esac
  1. Note 404. On GitHub a private repository you cannot see returns 404, not 403, on purpose, so that the API does not leak the existence of private things. Your handler must not treat 404 as “definitely does not exist”.
  2. Step five, pagination in a loop. GitHub returns a Link header holding the next page’s address, and you follow it until there is no next.
url="https://api.github.com/repos/kedbyte/book/issues?per_page=100"
while [ -n "$url" ]; do
  curl -sS -D head.txt -o page.json \
    -H "Authorization: Bearer $GH_TOKEN" "$url"
  jq -r '.[] | "\(.number) \(.title)"' page.json
  url=$(tr -d '\r' < head.txt | grep -i '^link:' \
    | tr ',' '\n' | grep 'rel="next"' \
    | sed -e 's/.*<\(.*\)>.*/\1/')
  sleep 1
done
  1. Three things make that loop safe. It follows the server’s own link instead of building page numbers itself. It stops when there is no next link. And it pauses a second between pages, which is politeness in one word.
  2. The same call in httpie, which is friendlier to type and colours the output, though the plumbing is identical:
https api.github.com/repos/kedbyte/book \
  Accept:application/vnd.github+json \
  Authorization:"Bearer $GH_TOKEN"
  1. And the secret. Look at the two examples above: the token came from an environment variable, $GH_TOKEN, never from a literal string in the file.
  2. That habit is what stops a token being committed to git, pasted into a bug report, or shipped inside a web page.

PLAIN36.13.4 what is really happening inside#

  1. A conditional request is a small conversation about versions.
  2. On the first response the server sends an ETag, which is just an opaque label for the exact bytes it sent. You store it beside the data.
  3. Next time you send If-None-Match with that label. The server compares. If the current label matches, it stops right there and sends 304 Not Modified with a status line, some headers, and no body at all.
  4. The saving is the body, which is usually 99 percent of the bytes, plus, on some APIs, the rate-limit charge.
  5. There is a second form, If-Modified-Since with a date, which is older and less precise because it only has one-second resolution. Prefer ETags.
  6. The same machinery, used the other way round, prevents lost updates. You send If-Match with the ETag on a write, and the server refuses with 412 Precondition Failed if somebody else changed the thing meanwhile.
  7. Pagination has two families, and knowing which you are in matters.
  8. Offset paging asks for “rows 200 to 299”. It is simple and it is wrong under change: if a row is inserted at the top while you are paging, everything shifts down by one and you see one item twice and miss another.
  9. Cursor paging asks for “carry on after this marker”. The marker encodes the position in a stable sort order, so insertions elsewhere do not disturb you.
  10. This is why big APIs moved to cursors, and why the Link header pattern hands you a complete next address instead of a page number to compute.
  11. Now the secret. When you put a key in browser JavaScript, the browser must be able to read it in order to send it. So the user can read it too.
  12. Minifying does not hide it. It is in the network tab of the developer tools the moment the request is made. In a mobile app it is in the binary, and strings on the file finds it in seconds.
  13. The correct shape is a small server of your own in the middle. The browser calls your server with the user’s own session. Your server holds the third party’s key and calls the third party. The key never leaves your machine.
  14. There is one legitimate exception, and it is a different kind of value: a publishable key designed to be public, such as a Stripe publishable key or a Maps key restricted by referring domain. These are identifiers, not credentials, and the vendor says so in the documentation. If the documentation does not say so, it is a secret.

TECHNICAL36.13.5 the engineer’s version#

  1. Tooling, and what each is genuinely for:
Tool Strength Weakness
curl Exact bytes, everywhere Verbose to type
httpie Readable, JSON-aware Extra install
Postman Collections, teams Heavy, cloud-linked
jq Filtering JSON in shell Own syntax to learn
  1. curl flags worth memorizing: -i include response headers, -D file dump headers to a file, -o file body to a file, -s silent, -S still show errors, -w '%{http_code}' print the status, -L follow redirects, --fail-with-body return a non-zero exit code on 4xx and 5xx while still printing the body, --compressed request gzip, -v full trace, and --connect-timeout with --max-time for deadlines.
  2. --fail-with-body, added in curl 7.76 (March 2021), is the flag that fixes shell scripts which silently succeed on an error response. Plain -f discards the body, which throws away the reason.
  3. Conditional requests are specified in RFC 9110, June 2022, sections 8.8 and
    1. ETag may be strong, "abc", or weak, W/"abc". Weak means semantically equivalent but not byte-identical, which is why weak validators are valid for caching but not for If-Match on writes.
  4. Caching headers you should actually read: Cache-Control with max-age, no-cache (revalidate, do not blindly reuse), no-store (do not keep at all), and private versus public. RFC 9111 is the caching specification. no-cache and no-store mean different things and are constantly confused.
  5. Rate-limit handling, in the order GitHub itself recommends: if a retry-after header is present, wait that long; else if x-ratelimit-remaining is zero, wait until x-ratelimit-reset; else wait at least one minute and then back off exponentially. GitHub states plainly that continuing to call while limited may get your integration banned.
  6. Concurrency is where well-behaved clients go wrong. Two parallel workers share one quota, so eight workers exhaust a 5,000-per-hour budget eight times faster. Rate-limit your own client, do not rely on being refused.
  7. Pagination in the wild:
Style Request looks like Trouble
Page number ?page=3&per_page=100 Shifts as data changes
Offset ?offset=200&limit=100 Same shift, slow deep
Cursor ?after=Y3Vyc29yOjIwMA Opaque, cannot jump
Link header follow rel="next" Must parse the header
  1. The Link header is RFC 8288, October 2017, Web Linking. GitHub sends rel="next", rel="prev", rel="first" and rel="last", and its guidance is to follow the header rather than construct page URLs yourself.
  2. Client-side secrets, precisely. A single-page app, a mobile binary, a desktop Electron app and a browser extension are all public clients in OAuth terms: they cannot keep a secret. RFC 9700, the January 2025 Best Current Practice for OAuth 2.0 Security, requires public clients to use the authorization code flow with PKCE and forbids the implicit grant, exactly because there is nowhere safe to put a client secret.
  3. Detection and cleanup: gitleaks and trufflehog scan repositories, GitHub push protection blocks known secret formats at push time, and GitHub secret scanning notifies partner providers so leaked keys can be revoked automatically. Rotating a leaked key is not optional; git history is permanent, and removing the file does not remove the commit.
  4. Server-side proxy pattern, in three rules: the browser authenticates to your server with the user’s own session; your server holds the third-party credential in a secrets manager or environment variable; and your server enforces its own rate limit and authorization, because otherwise you have simply published the upstream API with your key attached.
  5. Error handling that survives production: distinguish retryable from terminal (see section 36.10), log the request identifier the API returns, such as GitHub’s x-github-request-id, and quote it in support tickets. Never log the Authorization header, and be careful with -v, which prints it in full.
  6. Testing your client without the network: record and replay with VCR-style libraries, run a mock server generated from the OpenAPI document, or point at the vendor sandbox. Test the failure paths on purpose: 429, 500, timeouts, and a truncated response body.
  7. Observation: curl -w with a format string prints time_namelookup, time_connect, time_appconnect, time_starttransfer and time_total, which separates DNS from TCP from TLS from server think time, and turns “the API is slow” into a specific accusation.

WORDS36.13.6 remember these#

  1. Conditional request — asking only if it changed — a request carrying If-None-Match or If-Modified-Since, answered with 304 when unchanged.
  2. ETag — a label for one version of a resource — an opaque validator, strong or weak, compared by the server rather than interpreted by the client.
  3. 304 Not Modified — nothing new, keep what you have — a bodyless response confirming the client’s cached copy is current.
  4. Offset pagination — ask for rows N to M — index-based paging that skips or repeats items when the underlying data changes mid-scan.
  5. Cursor pagination — carry on after this marker — keyset paging using an opaque stable position, immune to concurrent insertions.
  6. Link header — the server tells you the next address — RFC 8288 web linking with relations such as next, prev, first and last.
  7. Public client — software that cannot keep a secret — any client whose code or binary is in the user’s hands, per RFC 6749 and RFC 9700.
  8. Backend for frontend — your own small server in the middle — a proxy holding third-party credentials so the user’s device never sees them.

36.14 APIs as a business#

PLAIN36.14.1 in simple words#

  1. So far an API has been an engineering thing. It is also a commercial thing, and some of the largest companies alive are shaped by that fact.
  2. A product is something people use. A platform is something people build on. An API is how a product becomes a platform.
  3. When other people build on you, they do work you did not pay for, and their customers become reachable by you. That is why a company gives data away.
  4. API-first means designing the interface before writing the software, and making your own teams use the same interface everyone else uses.
  5. That sounds like a small internal rule. In one famous case it reshaped an entire company and then created a new industry. Section 36.14.5 gives the dates.
  6. Money comes from APIs in several ways: charging per call, charging by tier, giving it away to sell something else, or taking a share of what flows through it.
  7. Then the uncomfortable part. If your business runs on somebody else’s API, you do not control your own product. They can change the price, change the rules, or switch it off.
  8. This is not a theoretical worry. Real companies have been closed by an announcement from a platform they depended on, and section 36.14.3 names them with years.
  9. The lesson is not “never build on an API”. Almost everything is built on somebody’s API. The lesson is to know exactly how exposed you are, and to price that risk instead of ignoring it.

PLAIN36.14.2 a picture in your head#

  1. Think of a railway company that has built a line between two cities.
  2. It could run only its own trains. It carries its own goods, it keeps all the revenue, and the size of the business is the size of its own trade.
  3. Or it can sell track access. Now other companies run trains on its rails. Each of them invests in carriages and customers that the railway never had to pay for.
  4. The railway earns a fee from every train, and the line becomes more valuable the more operators use it, because the destinations are better served.
  5. That is the platform move. The track is the API.
  6. Now notice the power that comes with owning track. The railway sets the access charge, the timetable slots, and the safety rules.
  7. An operator who has bought fifty carriages and hired three hundred staff cannot easily leave when the access charge triples. Their investment is welded to that particular line.
  8. And if the railway decides tomorrow to run only its own trains again, those fifty carriages are scrap metal.

Where this comparison breaks:

  1. Track takes years to build and is regulated by governments. An API can be changed in an afternoon by one team, and almost nowhere regulates it.
  2. A railway operator can see the rails and count the trains. An API user often cannot even tell how many other users there are, or whether they are a rounding error to the platform or its main source of value.

PLAIN36.14.3 a worked example#

  1. Here are real cases, with dates. Read them as case law, because that is effectively what they are.
Year Platform What changed
2014 Facebook Graph API Friend data cut off
2014 Netflix Public API closed
2018 Google Maps Price up about 14 times
2023 Twitter, now X Free access ended
2023 Reddit Charged for API calls
  1. Netflix, 2014. On 13 June 2014 Netflix announced that its public API would close, and it did close on 14 November 2014. Applications built on it, including catalogue browsers and recommendation tools, simply stopped.
  2. Facebook, 2014 to 2015. Graph API v2.0 arrived on 30 April 2014 and removed the broad access to a user’s friend list that many social apps had been built on. Version 1.0 was fully retired on 30 April 2015. A generation of “find your friends who also use this” features died in that change.
  3. Google Maps, 2018. Announced in early May 2018 and effective on 16 July 2018, the pricing moved from about 0.50 US dollars per 1,000 dynamic map loads to about 7.00 US dollars per 1,000, a rise of roughly fourteen times.
  4. The free allowance changed shape too: from 25,000 map loads per day to a 200 US dollar monthly credit, worth roughly 28,000 loads per month.
  5. Read those two numbers together. A site doing 25,000 loads a day was inside the free tier before, and afterwards was about 27 times over the monthly credit. Many small mapping sites moved to OpenStreetMap-based providers or closed the feature.
  6. Twitter, 2023. On 19 January 2023 the developer agreement was changed to ban third-party clients. Tweetbot and Twitterrific, which had existed since 2007 and 2008, shut down; Twitterrific had helped popularize the word “tweet” and the bird icon.
  7. In February 2023 free API access ended. On 29 March 2023 the new tiers arrived: a free tier limited to 1,500 posts per month and login, a Basic tier at 100 US dollars per month, and an Enterprise tier reported at around 42,000 US dollars per month.
  8. Academic research that had depended on cheap Twitter data largely stopped. That was not a side effect anybody had budgeted for.
  9. Reddit, 2023. Announced on 18 April 2023 and effective 30 June 2023. Christian Selig, the developer of the popular client Apollo, calculated that the new rate would cost him about 20 million US dollars per year.
  10. Apollo, Sync, BaconReader and rif is fun all shut down on 30 June 2023. Between 12 and 14 June 2023, thousands of subreddits went private in protest, and many stayed dark afterwards.
  11. Reddit’s chief executive, Steve Huffman, gave the reason openly at the time: the data was valuable, and giving it free to the largest companies in the world was no longer the plan. That is the honest commercial logic behind most of these changes, and it arrived alongside the demand for training data for language models.
  12. Now the pattern across all five. In every case the platform was inside its rights, the change was announced, the notice was months not years, and the businesses that died were the ones with no second source.

PLAIN36.14.4 what is really happening inside#

  1. Why would a company give its data away at all? Five reasons, and they are not charity.
  2. Distribution. Every app built on your API is a salesperson. Slack, Stripe and Twilio grew mostly this way.
  3. Lock-in through investment. Once a customer has written six months of code against your interface, moving costs them six months.
  4. Data. Every call tells you what people want, in aggregate, before your competitors know.
  5. Standards. If your shape of the problem becomes the shape everyone uses, the default choice is yours. Amazon’s S3 interface became the shape of object storage, and rivals now advertise being compatible with it.
  6. Complements. If maps are cheap, more apps need location, and location services are what you actually sell.
  7. Then the money models:
Model You pay Typical example
Free, funded elsewhere Nothing Search, social login
Freemium quota Above a limit Maps, weather
Per call Each request Messaging, translation
Tiered subscription Per month Data and analytics
Revenue share A cut of value Payments, marketplaces
  1. Revenue share is the strongest of these, because the API provider’s income grows with the customer’s success rather than with their request count. It is why payment APIs are such durable businesses.
  2. Now the risk, from the caller’s side. Ask four questions about any API you build on, before you build.
  3. What fraction of my product stops working if this disappears tomorrow?
  4. Is there a second supplier with a similar interface, and how many days of work is the switch?
  5. Am I paying? Free users are the first to be cut, because they are a cost with no contract.
  6. Is my usage growing towards a tier boundary that changes the price by a factor rather than a percentage?
  7. The mitigations are unglamorous and they work: put an interface of your own in front of theirs so the dependency touches one file rather than fifty; cache what you are permitted to cache; export your own data regularly; read the terms of service for the notice period; and keep a written estimate of the switching cost.
  8. The one that people skip is the first. If every part of your codebase calls the vendor’s SDK directly, then swapping vendors means editing everything. If they all call your own thin wrapper, it means editing one adapter.

TECHNICAL36.14.5 the engineer’s version#

  1. The Bezos mandate. Around 2002, Jeff Bezos issued an internal directive at Amazon that all teams must expose their data and functionality through service interfaces, must communicate only through those interfaces, and must design them to be externalizable, with termination as the stated consequence for non-compliance.
  2. Be honest about the source. Amazon never published the memo. The wording everyone quotes comes from a long public post written by the engineer Steve Yegge in October 2011, recalling it from his time at Amazon. So treat the exact phrasing as recollection, not as a document.
  3. The outcome, however, is checkable. Amazon Web Services launched publicly in 2006: Simple Storage Service in March 2006 and Elastic Compute Cloud in August 2006. A company that had already forced every internal team to speak only through network interfaces was in a position to sell those interfaces.
  4. That is the extreme case of taking internal APIs seriously, and it is the subject of Chapter 43, which covers cloud computing, the mandate, and how AWS and Azure turned internal engineering discipline into an industry.
  5. Metrics that API businesses actually run on, distinct from web metrics:
Metric What it measures
Time to first call Signup to first 200 response
Time to first value Signup to first real result
Calls per active key Depth of integration
Error rate by code Where users get stuck
Version distribution How much old shape to keep
  1. Time to first call is the one that predicts adoption. Stripe’s reputation was built substantially on making it a few minutes, with a working curl command on the front page of the documentation.
  2. API-first as an engineering practice means: design and review the interface document before implementation, generate server stubs and client libraries from it, run contract tests in continuous integration, and treat a breaking diff as a build failure. Chapter 41 covers the continuous integration machinery this depends on.
  3. The internal-external question is a genuine disagreement among architects. One camp says internal and external APIs should be the same interface, because dogfooding is the only reliable quality signal. The other says external APIs need stability guarantees, versioning, documentation and support that would paralyse internal development, so they should be a separate, deliberately narrower layer. Amazon’s mandate takes the first position; many large companies quietly take the second.
  4. Deprecation as a commercial act, not just a technical one. The pattern that holds up in court and in public opinion is: written notice period stated in the terms of service, public changelog entry with a date, Deprecation and Sunset headers from day one (RFC 9745 and RFC 8594), direct contact with accounts still calling the endpoint, and a migration guide.
  5. Legal reality, briefly and carefully. In Google versus Oracle America, decided by the United States Supreme Court on 5 April 2021, the court held that Google’s reuse of roughly 11,500 lines of Java SE API declaring code in Android was fair use. The court assumed without deciding that the declarations were copyrightable, so it did not settle whether API definitions can be copyrighted; it settled that this particular reuse was fair. Anyone telling you the case made APIs freely copyable is overstating it.
  6. Practical consequence of that case for engineers: reimplementing somebody else’s interface is a normal and long-standing practice, and it is how S3-compatible storage, PostgreSQL wire-protocol clones and POSIX reimplementations exist. It is not risk-free, and it is not settled law outside the United States.
  7. Failure modes of an API business, in one list: pricing that punishes exactly the users who grow; a free tier that attracts users the business model cannot support; breaking changes without a version; documentation that lags the implementation; and no sandbox, so nobody can evaluate you without committing.
  8. And the strategic trap on the other side: a platform that opens an API, lets an ecosystem grow, then builds the most profitable ecosystem product itself and restricts the interface that made the competitor possible. Every example in section 36.14.3 has some version of this in its history, and it is the strongest argument for owning your own second source.

WORDS36.14.6 remember these#

  1. Platform — something other people build on — a product whose value grows with third-party integrations rather than only with its own features.
  2. API-first — the interface is designed before the code — a practice where the specification is the source of truth and implementations are generated from or validated against it.
  3. Ecosystem — the businesses living on your interface — the set of third-party products whose viability depends on a platform’s continued access terms.
  4. Lock-in — leaving is expensive — accumulated switching costs from code, data, staff knowledge and contracts tied to one provider.
  5. Sunset, commercially — the announced end of an interface — the dated withdrawal of an API, governed by the terms of service notice period.
  6. Second source — an alternative supplier ready in advance — an equivalent provider behind your own abstraction layer, bounding your switching cost.
  7. Bezos mandate — talk only through interfaces — the roughly 2002 Amazon directive requiring all internal capability to be exposed as externalizable service interfaces, recounted publicly in 2011.

36.98 Common wrong ideas#

  1. Wrong: an API means a web endpoint. Right: an API is any published contract between software parts. strlen, write and Vulkan are APIs with no network anywhere near them.
  2. Wrong: REST means JSON over HTTP. Right: REST is six constraints defined by Roy Fielding in 2000 and says nothing about JSON. Almost every “REST API” in production is really an HTTP JSON API, because it omits the hypermedia constraint.
  3. Wrong: GraphQL replaces REST. Right: GraphQL solves over-fetching and round-trip count for client-driven queries, at the cost of caching, observability and query-complexity control. Many systems run both, for different callers.
  4. Wrong: an API key is authentication. Right: a key identifies an account. It rarely expires, is rarely scoped, is often committed to git or logged in a query string, and proves nothing about who is holding it right now.
  5. Wrong: versioning means adding v2 to the URL. Right: versioning is a policy about how long you will run old behaviour and who pays for it. The URL, a header, a media type and a pinned date are four ways to express that policy, and none of them is the policy itself.
  6. Wrong: a failed request means nothing happened. Right: a lost response is indistinguishable from a lost request at the client. The work may be done. That is partial failure, and the fix is idempotency, not a better error message.
  7. Wrong: retrying automatically is always safe. Right: retrying a non-idempotent request without an idempotency key can charge a customer twice, and retrying without jitter can turn a brief outage into a long one.
  8. Wrong: a JWT is encrypted, so claims inside it are private. Right: a JWT is base64url-encoded and signed, not encrypted. Anyone holding it can read every claim. The signature proves integrity, not secrecy.
  9. Wrong: the API key in my mobile app or single-page app is hidden. Right: any credential shipped to a user’s device is public. Minification, obfuscation and native binaries all lose to developer tools and the strings command.
  10. Wrong: my webhook handler is fine because it parses the JSON and then checks the signature. Right: the signature covers the exact bytes that arrived. Re-serializing changes the bytes and the check will fail, or worse, be skipped. Verify the raw body first.

36.99 Chapter summary in 20 lines#

  1. An API is a published contract about how one piece of software may ask another to do something: what you may ask, how, and what comes back.
  2. It is not a technology. It exists at every scale, from strlen inside one file to a service on the other side of the planet, and the idea never changes.
  3. An API has a surface, a shape and a behaviour. Beginners see only the surface; almost all real bugs live in the behaviour.
  4. Nobody invented APIs in one moment. Subroutine libraries appeared around 1951, the phrase “application program interface” in print in 1968, remote procedure calls with Birrell and Nelson in 1984, CORBA in 1991, SOAP and Fielding’s REST thesis around 2000, and GraphQL and gRPC from 2015.
  5. Every generation that tried to hide the network behind a local-looking call eventually failed at the same place: partial failure and latency.
  6. An API is not an ABI. An ABI fixes registers, calling conventions and struct layout, which is why code can compile and still crash at run time.
  7. Semantic versioning states the promise in three numbers: patch fixes, minor adds compatibly, major breaks. The promise is social, not enforced.
  8. Crossing a machine boundary through system calls costs a mode switch; crossing the planet costs about ten million times a local call.
  9. The eight fallacies of distributed computing, from Sun Microsystems between 1994 and 1997, each name a specific cost you pay for believing them.
  10. HTTP is a request line, headers, a blank line and an optional body, both ways. Methods differ in safety and idempotency; status codes are grouped 1xx to 5xx; HTTP/2 and HTTP/3 changed the framing, not the semantics.
  11. REST is six constraints, and the one almost nobody implements is the uniform interface’s hypermedia requirement. Say “HTTP JSON API” when that is what you mean.
  12. GraphQL, gRPC, SOAP, JSON-RPC, WebSockets and server-sent events each win on a different axis: payload size, typing, streaming, tooling or reach.
  13. Authentication asks who you are; authorization asks what you may do. API keys answer neither well. Bearer tokens expire, which is their main gain.
  14. OAuth 2.0 with PKCE, now required for all client types by RFC 9700 of January 2025, lets an app act for you without ever seeing your password.
  15. A JWT is a signed, readable token, not an encrypted one, and a scope limits the token rather than the user.
  16. Reliability is six habits: rate limits with token or leaky buckets, retries with exponential backoff and jitter, idempotency keys, timeouts at every layer, circuit breakers, and graceful degradation.
  17. Retrying a non-idempotent request without an idempotency key can double charge a real person, and retrying without jitter can extend an outage.
  18. Webhooks invert the call: the server posts to you. Verify the HMAC over the raw body, deduplicate on the event identifier, expect any order, answer fast, and use a reverse tunnel to receive them behind NAT.
  19. A usable API is consistent, has machine-readable error codes, an honest versioning and deprecation policy with Deprecation and Sunset headers, an OpenAPI description, generated SDKs and a sandbox.
  20. Commercially, an API turns a product into a platform, and the shutdowns of Netflix in 2014, Google Maps pricing in 2018, Twitter and Reddit in 2023 show what a platform’s decision can do to businesses built on it. Amazon’s roughly 2002 internal mandate is the extreme case, and Chapter 43 follows it.