Vocabulary for this course

The terms the chapters assume, in plain words. You can open this from any chapter.

backoff
Waiting longer between each retry, so a service that is struggling gets room to recover.
context window
The most text, counted in tokens, that a model can consider in a single request.
cost
The money a request is billed for, usually based on how many input and output tokens it uses.
idempotency
The property that repeating the same request has the same effect as sending it once.
jitter
A small random amount added to a wait, so many clients do not retry at the same moment.
latency
The time from sending a request to receiving the full answer, including time spent waiting in a queue.
prompt caching
Reusing the stored copy a service keeps of unchanged leading text so later requests cost less and start faster.
rate limit
A cap on how many requests you may send in a period, after which the service refuses more.
schema
A written description of the fields a response must contain and the type of each value.
streaming
Receiving an answer in pieces as it is produced instead of waiting for the complete response.
structured output
A response constrained to a schema, so every field and type can be checked and used by code.
timeout
A limit on how long a request may wait before your code gives up and treats it as failed.
token
A small unit of text, such as part of a word, a whole word or punctuation, that a model reads and bills for.
tool call
A request from a model for your code to run a named function with arguments, with the result sent back.

LLM APIs from the ground up

Capstone: a resilient client

One client that times out, retries with jitter, respects rate limits, validates output, accounts for cost and survives a scripted failure. Every chapter of the course, once.

Part 10 of 10

Advanced18 min readBack to Courses

The shape of the client

A request goes through a small pipeline: build the body, check the local rate limiter, send with a deadline, classify the failure, retry or fail, validate the response, record the tokens and the cost. Each stage is ordinary code and each one is testable on its own. The pipeline is also where you put the product rules, such as “this feature may spend at most two cents per request”.

The transport is the seam. Put the network behind an interface and a scripted transport can inject a 429, a timeout and a malformed response without a provider, an API key or a flaky connection. The seam is what makes the failure paths testable at all; without it, every test of a retry is really a test of the network.

The pipeline is also where a product rule becomes code. “Never spend more than two cents on a chat turn” is a check before the call and a counter after it, not a sentence in a design document. The same is true of “never retry a charge” and “always show a partial answer as partial”: each is a branch in the client.

Failure is the normal path

A client that only works when the provider is healthy is a demo. The resilient version has a bounded number of attempts, a deadline that covers all of them, and a clear final error that says what was tried. The caller should never see a raw socket exception. The error should carry the last status, the number of attempts and the time spent, because that is what the incident review will ask for.

The same applies to partial success. A stream that dies halfway, a tool call that returns an error, a JSON body that fails validation: each is a state the caller can handle if the client names it, and an outage if the client throws something generic. Naming the states is most of the design work; the retry policy is comparatively small.

Let the SDK do the boring part

from openai import OpenAI

client = OpenAI(
    timeout=20.0,
    max_retries=3,          # retries 408, 409, 429 and 5xx
)

response = client.chat.completions.create(
    model="gpt-4.1-mini",
    messages=messages,
    max_tokens=400,
)

A client that survives a bad day

Run the scripted transport, then add a deadline in seconds and make one attempt sleep past it. Confirm the client gives up with a named error instead of hanging.

Output
Click "Run" to execute your code

Measure the client, not the model

Success rate, attempts per success, p95 latency and cost per success are client metrics. They tell you whether a change to the retry policy helped, which a model benchmark cannot. A retry that raises the success rate from 97 to 99.5 percent may also double the cost, and only the client can see both. The four numbers belong on one chart, because improving one at the expense of another is the usual trade.

Run the scripted transport in CI with a fixed seed. A chaos test that is deterministic is a regression test; one that is random is a demo that sometimes fails. The script should include the ugly cases: a 429 followed by a 500, a stream that ends early, and a body that parses but fails the schema.

Give every request a deadline and every retry a reason. A retry log that says “503, attempt 2 of 4, waited 1.2s” is worth more during an incident than any dashboard.

What is still missing

A production client also needs observability, a circuit breaker, a queue for work that can wait, and a budget that spans a user session rather than a single request. Each is an addition to a stage that already exists. None of them requires a new architecture, which is the point of building the pipeline first.

The point of the capstone is that none of it is mysterious. A timeout, a bounded retry, a limiter, a schema and a cost counter are five small pieces of ordinary code, and together they are the difference between calling an API and running a service. The model is the part you rent; the client is the part you own.

The client is also the part you can test without a provider, which makes it the part worth investing in first. A model upgrade is a config change; a client that handles timeouts, partial streams and bad JSON is the thing that keeps the product working when the upgrade is not the only thing that changed.

Run the client under scripted failure

Run the table, then raise max_attempts to 8 and find the failure rate where success stops improving. Compare the cost at each setting.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

Why put the network behind a transport interface?