Foundations: Python, Git and toolsLoading progress…

Errors and retries

Send a real request to a model from the provider of your choice, then make the surrounding program trustworthy: constrain the output, validate it, handle streaming and failures, and keep cost bounded.

Some failures heal, some never will

A model API is a service on a shared network, so calls fail. The question is not whether to retry but which failures to retry. A rate limit clears as time passes. An overloaded server recovers. A wrong key does not become right because you asked again.

Pick a status code and decide before you read the reason.

Predict first. A request fails with 401 Unauthorized. What should the client do?

Exponential backoff with jitter

Retry with growing waits: the k-th retry waits base x 2^k seconds. Doubling gives a struggling server room to recover. Cap the number of retries, and when the server sends a Retry-After value, prefer it over your own guess.

Add jitter, a random factor on each wait. Without it, every client that failed at the same moment retries at the same moment, and the recovering server is hit by a synchronised crowd each round.

One client: when each retry is sent (attempt 0 fails at t = 0)

#1 0.50s#2 1.50s#3 3.50s#4 7.50s

Planned waits without jitter: 0.5s, 1s, 2s, 4s (base x 2^k). Total waiting before the last retry: 7.5s. With jitter each wait is multiplied by a random factor between 0.5 and 1.

Twelve clients, one outage: each row is a client, each dot a retry

Busiest 0.1 second window: 12 of 12 clients retrying at once. Every client hits the recovering server together, again and again.

Turn jitter off and on. The upper timeline is one client; the dots below are twelve clients that all failed at t = 0.
Chapter context

A model API turns input messages into generated output, but the program around it stays responsible for validation, retries and side effects. Every major provider (Anthropic, OpenAI, Google, Mistral, Groq, OpenRouter and local OpenAI-compatible servers) exposes the same few ideas under different field names: a model id, a list of messages with optional system instructions, an output-token limit, and a response carrying the text, a reason it stopped, and token usage. The labs in this chapter run against whichever provider you choose with your own key, so you see real behavior rather than a mock.

Chapter reference: math, worked example & code

This section belongs to the whole chapter, so it is the same on every lesson in it and may cover a different lesson's topic. The code for the experiment you just ran is inside the experiment itself.

Math and notation

Four small rules recur through the chapter.

  • Complete or cut off (F8.1): treat the reply as an answer only when the provider's stop reason means a natural stop (end_turn, stop, STOP), not a limit (max_tokens, length, MAX_TOKENS).
  • Validation order (F8.2-F8.4): parse, then check fields, then check domain rules. Passing one stage says nothing about the next.
  • Backoff (F8.5): wait dk=b⋅2k⋅jd_k=b\cdot 2^k\cdot j before retry kk, where bb is the base delay and the jitter factor jj is a random number in [0.5,1][0.5,1] (or 11 without jitter). Retry only temporary errors, and stop after a fixed number of retries.
  • Cost (F8.6): for prices pip_i and pop_o per million input and output tokens, and token counts nin_i and non_o:
C=(nipi+nopo)/106.C=(n_i p_i+n_o p_o)/10^6.

With an output cap m≥nom\ge n_o, the worst-case output side for NN calls is N m po/106N\,m\,p_o/10^6. The prices below are invented arithmetic inputs, not a product recommendation; use the provider's current price list for real budgeting.

Worked example

One small helper per idea, all checked offline. A reply stopped with end_turn is usable but one stopped with max_tokens is not. The text {"sentiment": "neutral", "confidence": 0.7} passes all three validation stages, while a confidence of 7 or a reply with chatty text around the JSON is rejected. A 429 is retried but a 401 is not, and the delays for retries 0, 1 and 2 at a 0.5 s base are 0.5, 1.0 and 2.0 seconds. At illustrative prices of 2and2 and 6 per million tokens, a call with 1,000 input and 500 output tokens costs (1000×2 + 500×6)/1,000,000 = $0.005.

Reference code

The following example uses the Python standard library and does not call a remote service. It is the same logic the labs let you try against a real model.

import json
from math import isclose

def usable(stop_reason):                      # F8.1: a limit means the text may be cut off
    return stop_reason in {"end_turn", "stop", "STOP"}

def validate(text):                           # F8.3: syntax, then fields, then rules
    data = json.loads(text)                   # raises ValueError on bad syntax
    if data.get("sentiment") not in {"positive", "negative", "neutral"}:
        raise ValueError("bad sentiment")
    if not 0 <= data.get("confidence", -1) <= 1:
        raise ValueError("confidence out of range")
    return data

def should_retry(status, attempt, max_retries=3):   # F8.5: temporary errors only, bounded
    return status in {429, 529} and attempt < max_retries

def delay(attempt, base=0.5, jitter=1.0):     # F8.5: exponential backoff
    return base * 2 ** attempt * jitter

def cost(n_in, n_out, p_in=2, p_out=6):       # F8.6: illustrative prices
    return (n_in * p_in + n_out * p_out) / 1_000_000

assert usable("end_turn") and not usable("max_tokens") and not usable("length")
assert validate('{"sentiment": "neutral", "confidence": 0.7}')["confidence"] == 0.7
for bad in ['{"sentiment": "neutral", "confidence": 7}', 'Sure! {"sentiment": "neutral"}']:
    try:
        validate(bad)
    except ValueError:
        pass
    else:
        raise AssertionError("accepted bad output")
assert should_retry(429, 0) and not should_retry(401, 0) and not should_retry(429, 3)
assert [delay(a) for a in range(3)] == [0.5, 1.0, 2.0]
assert isclose(cost(1000, 500), 0.005)
assert isclose(cost(1000, 500) * 10_000, 50)

Python examples are provided for study and local execution; this page does not execute Python.

Common mistakes

JSON output can still be false or out of range. A model call is not required for every rule-based task. Never place a reusable private API credential in a public browser bundle: the labs here use a key you paste yourself, kept in your browser and sent only to the provider, and a shipped product would call the provider from a server instead. Reasoning models can spend part of the output limit on hidden reasoning, so a tiny limit may return empty text.