AI EngineeringZero to ProductionHome·About·Contact
Developer Foundations · Chapter DF4

HTTP, DNS, REST & JSON

The plumbing under every API and LLM call — DNS, HTTP, REST, JSON — climbing to a resilient, reusable client with retries, rate-limit handling, and pagination. This is the SDK, unwrapped.

⏱️ ~3.5 hours🧪 9 labs🎯 Beginner→Tech-lead
🌱 Start here — from zero How the web works, from scratch — what happens between a URL and a response — the plumbing under every API and LLM call.

Building an API, calling an LLM, or deploying a service all use the web's plumbing: DNS, HTTP, REST, JSON. This chapter demystifies each, then climbs to a resilient, reusable API client. One block makes a real call (marked); the rest run offline using stdlib and fakes, so every runnable example works with zero errors.

The words you'll hear (in plain terms):

TermWhat it actually means
HTTPthe request/response protocol (methods, status codes, headers).
DNSturns a name (example.com) into an IP address.
RESTa convention for APIs over HTTP verbs on resource URLs.
JSONthe text format structured data travels in.
idempotentsafe to retry (GET); POST is not.

What you need before starting:

  • Python basics; pip install requests for the one live call.
  • Having called an API helps but isn't required.
  • Runnable blocks are offline; the one network call is labeled.

New to the topic? Read this box, then take the chapters in order — each section is tagged essentialexpert so you always know the depth you're at.

Learning objectives

  • Trace DNS → TCP → TLS → HTTP for a request.
  • Use methods, status codes, headers, and JSON from Python.
  • Build a resilient client with retries, backoff, and timeouts.
  • Standardize a reusable, testable client for a team.
▶ Runnable companionCode saved under code/df4-how-web-works/. Python runs offline; configs are ready to use.

1 · What happens when you hit a URL essential

Type example.com → the browser resolves DNS to an IP, opens a TCP connection, negotiates TLS (for https), sends an HTTP request, and gets a response (status + headers + body). That round trip is the whole web.

Type URL example.com DNS → IP the phone book TCP+TLS connect the pipe HTTP req/resp GET / → 200
🗺️ How to read this diagram

This strip shows what really happens in the split second between typing a web address and seeing a page. Read it left to right — each box is one step, and the arrows are the order they happen in. Every API call and every LLM call goes through this same chain.

  • Box 1 · Type URL (example.com) — the human-friendly name you type. A name is not an address the network can use yet, so it must be looked up.
  • Box 2 · DNS → IP ("the phone book") — DNS is the internet's phone book: it translates the name example.com into a numeric IP address (like 93.184.216.34) that identifies the actual server machine.
  • Box 3 · TCP+TLS connect ("the pipe") — your computer opens a connection to that IP. TCP is the reliable pipe the data flows through; TLS is the lock that encrypts it (that's the 's' and the padlock in https).
  • Box 4 · HTTP req/resp (GET / → 200) — now the real conversation: your browser sends an HTTP request ("GET me the home page") and the server sends back a response. 200 is the status code meaning "OK, here it is".
  • The arrows show this is a sequence — you can't connect before you know the IP, and you can't send the request before the pipe is open. Name → address → pipe → conversation.

In short: A URL is just a name; the network needs an address (DNS), a pipe (TCP+TLS), then a request/response (HTTP). Every SDK you'll use — including the LLM one — hides these four steps behind a single function call.

2 · HTTP methods & status codes essential

MethodMeansIdempotent?
GETreadyes (safe to retry)
POSTcreateno
PUTreplaceyes
PATCHupdate partno
DELETEremoveyes
StatusMeans
2xxsuccess (200 ok, 201 created, 204 no content)
3xxredirect (301 moved, 304 not modified)
4xxyou erred (400 bad, 401 unauth, 403 forbidden, 404 missing, 429 rate-limited)
5xxserver erred (500, 502 bad gateway, 503 unavailable)

3 · JSON — how data travels essential

JSON is the API lingua franca. Python's json maps it to dicts/lists and back.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Python · parse and build JSON (runs)
json_demo.pyimport json
data = json.loads('{"user":"ava","roles":["dev","admin"],"active":true,"age":30}')
print(data["roles"][0], "| active:", data["active"], "| type:", type(data["active"]).__name__)
print(json.dumps({"ok": True, "items": [1, 2]}, indent=2))
dev | active: True | type: bool
{
  "ok": true,
  "items": [
    1,
    2
  ]
}
▶ How this works

JSON is the text format that data travels in between programs — almost every API sends and receives JSON. This tiny program does both directions: it reads a JSON string into Python, then writes Python data back out as JSON.

  1. json.loads(...) means "load from string": it takes the JSON text (the part in quotes) and turns it into normal Python values — a dict here. Now you can look things up like any dictionary.
  2. data["roles"][0] reads the first role out of the list, and type(data["active"]).__name__ shows that JSON true became a real Python bool — the types are converted for you, not left as text.
  3. json.dumps(...) is the reverse — "dump to string": it turns a Python dict back into JSON text. indent=2 pretty-prints it with 2-space indentation so it's easy to read.

What the output means: The first line proves the values came through as real Python types (active: True is a bool, not the word "true"). The rest is the same data printed back as neat, indented JSON — note JSON writes it lowercase true.

Try this: Add "score": 9.5 inside the input string and print data["score"] — you'll get a Python float. JSON numbers, strings, lists, and true/false all map to the matching Python types automatically.

4 · Make a real request intermediate

The requests library makes HTTP trivial — this is what the LLM/AWS SDKs do under the hood. This one block hits the network.

Python · GET + POST (needs internet)
live_request.pyimport requests
# NOTE: makes real network calls — run where you have internet.
r = requests.get("https://httpbin.org/get", params={"q": "hello"}, timeout=10)
print("status:", r.status_code, "| args:", r.json()["args"])
r = requests.post("https://httpbin.org/post", json={"name": "Ava"}, timeout=10)
print("server saw:", r.json()["json"])
status: 200 | args: {'q': 'hello'}
server saw: {'name': 'Ava'}
▶ How this works

This is the one block that actually touches the internet. It uses the popular requests library to make two real HTTP calls to httpbin.org, a free site that simply echoes back whatever you send — perfect for seeing how requests work.

  1. requests.get(url, params={...}, timeout=10) makes a GET request — GET means "read / fetch something". The params become the ?q=hello part of the URL (the query string). timeout=10 means "give up after 10 seconds" so your program never hangs forever.
  2. r.status_code is the status code200 means success. r.json() parses the response body (which is JSON) into a Python dict, and ["args"] pulls out the query values the server saw.
  3. requests.post(url, json={...}) makes a POST request — POST means "send / create something". The json= argument sends your dict as a JSON body; the echo server hands it right back so you can confirm it arrived.

What the output means: status: 200 confirms the call succeeded, and the echoed args and json show the server received exactly what you sent — proof the round trip worked.

Try this: Change "q": "hello" to your own word and re-run — the server will echo it straight back. This GET-plus-POST pattern is the core of every API you'll ever call.

Everything below runs offlineReal HTTP needs a network, so that call is marked. The rest of the chapter's runnable code uses stdlib + fakes so the logic (status handling, retries, a client) executes anywhere with zero errors.

5 · Status-handling logic intermediate

Branch on the status: 200 use it, 404 is a normal "not found", unexpected 4xx/5xx raise. Here's that logic as a pure function — offline-runnable and exactly what you wrap around a response.

Python · handle responses (runs offline)
status.pyclass Resp:                       # stand-in for requests.Response
    def __init__(self, status, data=None): self.status_code, self._d = status, data
    def json(self): return self._d
    def raise_for_status(self):
        if self.status_code >= 400: raise RuntimeError(f"HTTP {self.status_code}")

def handle(r):
    if r.status_code == 200: return r.json()
    if r.status_code == 404: return None          # normal outcome
    if r.status_code == 429: return "RATE_LIMITED" # back off + retry
    r.raise_for_status()                           # unexpected -> raise

print("200 ->", handle(Resp(200, {"ok": True})))
print("404 ->", handle(Resp(404)))
print("429 ->", handle(Resp(429)))
try: handle(Resp(500))
except RuntimeError as e: print("500 ->", e)
200 -> {'ok': True}
404 -> None
429 -> RATE_LIMITED
500 -> HTTP 500
▶ How this works

A response isn't always a success — you must decide what to do based on the status code. This shows that decision logic as one small function. It uses a fake Resp object instead of a real call so it runs anywhere with no internet.

  1. class Resp is a pretend response with just a status_code and some data — enough to test the logic. raise_for_status() imitates the real library: it throws an error when the status is 400 or higher.
  2. Inside handle(r) the code branches on the status: 200 → return the data (success); 404 → return None because "not found" is a normal outcome, not a crash; 429 → return a signal meaning "you're rate-limited, back off and retry".
  3. Any other bad status falls through to r.raise_for_status(), which raises — because an unexpected 500 is a real problem you want to hear about, not swallow silently.

What the output means: Each line shows the branch that fired: 200 gives the data, 404 gives None, 429 gives RATE_LIMITED, and 500 is caught as a raised HTTP 500 error.

Try this: Add a line print(handle(Resp(201))). 201 ("created") isn't handled, so it falls through to raise_for_status — showing you'd want to treat other 2xx codes as success too.

6 · Advanced — retries with exponential backoff advanced

Networks fail transiently. Production clients retry with exponential backoff (and jitter), cap attempts, and only retry retryable errors (timeouts, 5xx, 429 — not a 400). The resiliency habit from py4.

Python · retry/backoff, proven with a fake (runs)
retry.pydef with_retry(fn, tries=4, base=0.01):
    for attempt in range(tries):
        try:
            return fn()
        except (TimeoutError, ConnectionError) as e:
            if attempt == tries - 1: raise
            delay = base * (2 ** attempt)          # 0.01, 0.02, 0.04 (+ jitter in prod)
            print(f"  attempt {attempt+1} failed ({e}); backing off {delay:.3f}s")

calls = {"n": 0}
def flaky():
    calls["n"] += 1
    if calls["n"] < 3: raise ConnectionError("transient")
    return "ok"

print("result:", with_retry(flaky), "after", calls["n"], "attempts")
  attempt 1 failed (transient); backing off 0.010s
  attempt 2 failed (transient); backing off 0.020s
result: ok after 3 attempts
▶ How this works

Networks fail randomly — a call that failed a second ago often works if you just try again. This retry with backoff wrapper re-runs a function a few times, waiting a little longer between each attempt, instead of giving up on the first hiccup.

  1. with_retry(fn, tries=4, base=0.01) takes the function to run (fn), how many attempts to allow, and a base wait time. The for attempt in range(tries) loop is the "try again" mechanism.
  2. try: return fn() attempts the call; if it works, we return immediately. The except (TimeoutError, ConnectionError) only catches transient network errors — the kind worth retrying. On the last attempt it raises so failures aren't hidden.
  3. delay = base * (2 ** attempt) is exponential backoff: the wait doubles each time — 0.01, then 0.02, then 0.04 seconds — so you don't hammer a struggling server. (Real code adds a little random "jitter" too.)
  4. The flaky() test function fails the first two times, then succeeds — a controlled way to prove the retry loop actually recovers.

What the output means: You see two "attempt failed … backing off" lines (the doubling delays), then result: ok after 3 attempts — the third try succeeded, exactly as designed.

Try this: Change if calls["n"] < 3 to < 5. Now it fails more times than tries allows, so the retries run out and the error is raised — showing why capping attempts matters.

7 · Advanced — rate limiting & pagination advanced

Two realities of real APIs: they rate-limit you (respect 429 + Retry-After), and they paginate large results (follow next cursors/pages). Handle both or you'll get partial data and bans.

Python · consume a paginated API (runs offline)
pagination.py# A fake paginated endpoint: 3 pages of results.
PAGES = {
    None:   {"items": [1, 2], "next": "p2"},
    "p2":   {"items": [3, 4], "next": "p3"},
    "p3":   {"items": [5],    "next": None},
}
def fetch_page(cursor): return PAGES[cursor]

def fetch_all():
    items, cursor = [], None
    while True:
        page = fetch_page(cursor)
        items += page["items"]
        cursor = page["next"]
        if cursor is None: break
    return items

print("all items across pages:", fetch_all())
all items across pages: [1, 2, 3, 4, 5]
▶ How this works

Big result sets don't arrive all at once — APIs send them in pages, and each page tells you where the next one is. This shows how to follow those pages until there are none left, using a fake endpoint so it runs offline.

  1. PAGES is a fake API: each page holds some items plus a next pointer (a cursor) to the following page. The last page's next is None, meaning "you're done".
  2. fetch_all() starts with an empty list and cursor = None (the first page). The while True loop fetches a page, adds its items with items += page["items"], then moves the cursor forward to page["next"].
  3. if cursor is None: break is the stop condition — when a page reports no next cursor, the loop ends and we return everything collected. This is exactly how you'd walk a real API's pages without missing or repeating data.

What the output means: all items across pages: [1, 2, 3, 4, 5] — the loop stitched three separate pages into one complete list.

Try this: Add a fourth page "p3": {"items": [5], "next": "p4"} and a "p4" page ending in None. The same loop follows the extra page automatically — you never hard-code how many pages there are.

8 · Professional — a reusable client class professional

Teams don't scatter requests.get everywhere — they wrap an API in a client class with base URL, auth, timeouts, and error handling in one place (SD3 SOLID), with an injectable session so it's testable without a network.

Python · one client, tested with a fake session (runs offline)
api_client.pyclass ApiClient:
    def __init__(self, base_url, token=None, timeout=10, session=None):
        self.base_url = base_url.rstrip("/"); self.timeout = timeout
        self.session = session                      # inject -> testable
        if token and session: session.headers["Authorization"] = f"Bearer {token}"
    def get(self, path, **params):
        r = self.session.get(f"{self.base_url}/{path.lstrip('/')}",
                             params=params, timeout=self.timeout)
        r.raise_for_status(); return r.json()

class FakeResp:
    status_code = 200
    def json(self): return {"user": "ava"}
    def raise_for_status(self): pass
class FakeSession:
    headers = {}
    def get(self, url, params=None, timeout=None):
        print("would GET:", url); return FakeResp()

client = ApiClient("https://api.example.com", token="secret", session=FakeSession())
print("result:", client.get("users/1"))
would GET: https://api.example.com/users/1
result: {'user': 'ava'}
▶ How this works

Instead of scattering requests.get calls all over a codebase, teams wrap an API in one client class that keeps the base URL, auth token, and timeout in a single place. This is the same shape as the official LLM and AWS SDKs.

  1. __init__ stores the settings once: base_url (with any trailing slash stripped), a timeout, and a session. If a token is given, it's added as an Authorization: Bearer … header so every request is authenticated automatically.
  2. The session is injected (passed in) rather than created inside — that's the key trick. In production you pass a real network session; in a test you pass a fake one, so the class is testable without any internet.
  3. get(self, path, **params) builds the full URL, calls the session's get, then r.raise_for_status() turns any error status into an exception before returning r.json() — so callers always get clean data or a clear failure.
  4. FakeSession and FakeResp are stand-ins: FakeSession.get just prints the URL and returns a canned {"user": "ava"}. That's how you prove the client's logic works with zero network calls.

What the output means: would GET: https://api.example.com/users/1 shows the URL the client built, and result: {'user': 'ava'} is the parsed data — all from the fake, no internet needed.

Try this: Add a post method that mirrors get but calls self.session.post. One class now owns every call's URL, auth, and error handling — the whole point of a reusable client.

9 · Tech-lead — standardize reliability tech-lead

This IS the LLM SDK unwrapped: every client.messages.create(...) is an HTTP POST with an auth header + JSON body, status-checked, retried. A lead defines one resilient client (retries, timeouts, logging, rate-limit handling) so every integration inherits the same reliability.

One client, whole-codebase reliabilityWhen a lead ships a single hardened ApiClient and the team uses it everywhere, every external call gets retries, timeouts, and consistent error handling for free. That's how you make an entire codebase robust with one class.

Exercise DF4.1 — Build a resilient client

Context: The capstone of the chapter is a single client that behaves well against a real, flaky network: it retries the right failures, times out, paginates, and — crucially — is fully testable without a network.

Your task: Wrap a public API in an ApiClient with retries + backoff, a timeout, 404→None and 429→retry handling, pagination, and an injectable session so you can unit-test it with a fake.

Requirements:

  • Add retries with backoff plus a request timeout
  • Map 404 to None and treat 429 as a retryable case
  • Page through a multi-page response until it's exhausted
  • Inject the session/transport so the network seam is swappable for tests (TQ3)
  • Prove the retry logic with a fake that fails twice then succeeds, and page through a multi-page fake

💡 Hint: Design against a fake session first: if your client can be fully exercised offline — flaky retries and pagination included — the real requests.Session is just a drop-in.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Read a status codeBeginner

Context: Every HTTP response carries a status code, and its first digit tells you the whole story: success, redirect, your mistake, or the server's. Knowing which codes are worth retrying is the foundation of every resilient client.

Your task: Without any network, write a function that maps an HTTP status code to its class (informational / success / redirect / client-error / server-error) and whether a client should retry. Test it on 200, 301, 404, and 503.

Requirements:

  • Derive the class from the first digit (e.g. code // 100)
  • Return a retryable flag that is true only for transient codes (429, 500, 502, 503, 504)
  • Show 404 classed as client-error and not retryable
  • Show 503 classed as server-error and retryable
  • Print the classification for each of 200, 301, 404, 503

💡 Hint: The mental model: 4xx means you got the request wrong (don't retry, fix it); 5xx means the server failed (retrying may help).

Show solution

Pure Python, no network needed:

def classify(code):
    klass = {1: "informational", 2: "success", 3: "redirect",
             4: "client-error", 5: "server-error"}[code // 100]
    retryable = code in (429, 500, 502, 503, 504)
    return klass, retryable

for c in (200, 301, 404, 503):
    print(c, classify(c))
# 200 ('success', False)
# 301 ('redirect', False)
# 404 ('client-error', False)   -> your bug, do not retry
# 503 ('server-error', True)    -> transient, retry with backoff

4xx means you got it wrong (fix the request); 5xx means the server failed (retrying may help).

Exercise 2 · Round-trip JSONIntermediate

Context: Data travels between services as JSON text, and JSON only knows a handful of types. Understanding what survives a round-trip — and what doesn't — prevents a whole class of serialization bugs.

Your task: Build a small dict, serialize it to a JSON string, parse it back, and confirm the round-trip preserves types — then show one gotcha: JSON has no tuple or datetime.

Requirements:

  • Serialize with json.dumps and parse back with json.loads
  • Confirm str/list/bool/float survive the round-trip
  • Show that a datetime/date raises TypeError until converted
  • Note that tuples come back as lists
  • State the fix: convert dates/sets/bytes to JSON-native types before sending

💡 Hint: Try serializing a value JSON can't represent and catch the TypeError — the error message itself points you at what needs converting first (e.g. isoformat()).

Show solution
import json
from datetime import date

payload = {"user": "ada", "roles": ["admin", "dev"], "active": True, "score": 3.5}
text = json.dumps(payload)          # dict  -> JSON string (what goes on the wire)
back = json.loads(text)             # JSON string -> dict
print(back == payload)              # True: str/list/bool/float survive

# gotcha: no native tuple or datetime -- you must convert first
try:
    json.dumps({"when": date.today()})
except TypeError as e:
    print("needs conversion:", e)   # serialize as when.isoformat()

Tuples come back as lists; dates/sets/bytes must be converted to JSON-native types before sending.

Exercise 3 · Make a real requestAdvanced

Context: Talking to a live API is where theory meets the network's messiness: the call can come back with a bad status, or never come back at all. A real client must handle both without hanging or crashing.

Your task: Call a public API with requests, check the status, and parse the JSON — handling both failure modes: a non-2xx response and a network/timeout error.

Requirements:

  • Issue the request with requests.get and a timeout set
  • Turn 4xx/5xx into an exception with r.raise_for_status()
  • Catch requests.HTTPError for bad statuses and report the code
  • Catch requests.RequestException for DNS/timeout/connection errors
  • Return the parsed JSON on success, None on handled failure

💡 Hint: Always pass a timeout — without one a hung server hangs your program forever, which is the single most common production footgun here.

Show solution

Needs network + requests (pip install requests). Uses the free httpbin echo service:

import requests

def get_json(url, timeout=5):
    try:
        r = requests.get(url, timeout=timeout)
        r.raise_for_status()          # turns 4xx/5xx into an exception
        return r.json()
    except requests.HTTPError as e:
        print("bad status:", e.response.status_code)
    except requests.RequestException as e:
        print("network error:", e)    # DNS, timeout, connection refused
    return None

data = get_json("https://httpbin.org/json")
print(data and list(data.keys()))

Always set a timeout — without it a hung server hangs your program forever.

Exercise 4 · Retries with exponential backoffExpert

Context: Transient failures are normal at scale, so a good client retries — but only on the right failures, with growing delays and jitter so a fleet of clients doesn't retry in lockstep. Modelling it offline keeps the logic testable.

Your task: Wrap a flaky call so it retries only on retryable failures (429/5xx and network errors), backing off 1s, 2s, 4s… with jitter, and gives up after N attempts — modelled offline with a fake that fails twice then succeeds.

Requirements:

  • Retry up to a configurable number of attempts, then raise after the last one
  • Grow the delay exponentially (base * 2**i) and add random jitter
  • Only retry retryable failures — never a 4xx, whose request is itself wrong
  • Make it runnable offline by injecting/stubbing the sleep so it prints instead of waiting
  • Demonstrate with a fake that fails twice then returns success

💡 Hint: Jitter (the small random add on each delay) is what prevents a thundering herd of clients all retrying at the same instant.

Show solution

Runnable offline — the backoff sleep is stubbed so it prints instead of waiting:

import random

def with_retries(call, attempts=4, base=1.0, sleep=print):
    for i in range(attempts):
        ok, result = call()
        if ok:
            return result
        if i == attempts - 1:
            raise RuntimeError(f"gave up after {attempts} attempts")
        delay = base * (2 ** i) + random.uniform(0, 0.3)  # jitter avoids thundering herd
        sleep(f"retry {i+1} in {delay:.2f}s")

# fake service: fails twice (transient), then returns 200
_calls = {"n": 0}
def flaky():
    _calls["n"] += 1
    if _calls["n"] < 3:
        return False, 503
    return True, {"data": "ok"}

print(with_retries(flaky))   # prints two backoff lines, then {'data': 'ok'}

Jitter (the random add) prevents many clients retrying in lockstep. Never retry a 4xx — the request itself is wrong.

Exercise 5 · A reusable client classProfessional

Context: Once several call sites hit the same API, you want base URL, auth, timeout, and retries configured in one place. Making the network seam swappable is what lets you unit-test the client without touching the network.

Your task: Wrap a base URL, default headers, timeout, and retries into a small ApiClient class so callers just say client.get('/users/1') — keeping the transport swappable so it's testable offline.

Requirements:

  • Store base URL, default headers (Accept, optional Bearer auth), and timeout on the instance
  • Expose a simple get(path) that joins path to base and returns parsed JSON
  • Raise on error statuses (≥ 400) instead of returning a bad body
  • Inject the transport (a callable) so a fake can stand in for the network
  • Demonstrate the client working against an offline test double

💡 Hint: Centralizing base URL, auth, and timeout means every call is consistent and you change reliability policy in exactly one place — pass a real requests.Session in prod.

Show solution

The transport is injected, so the class is unit-testable offline; in production pass a real requests.Session.

class ApiClient:
    def __init__(self, base_url, token=None, timeout=5, transport=None):
        self.base = base_url.rstrip("/")
        self.headers = {"Accept": "application/json"}
        if token:
            self.headers["Authorization"] = f"Bearer {token}"
        self.timeout = timeout
        self.transport = transport      # callable(method,url,headers,timeout)

    def get(self, path):
        url = f"{self.base}{path}"
        status, body = self.transport("GET", url, self.headers, self.timeout)
        if status >= 400:
            raise RuntimeError(f"{status} for {url}")
        return body

# offline test double
def fake(method, url, headers, timeout):
    assert headers["Authorization"] == "Bearer t0k"
    return 200, {"id": 1, "name": "ada"}

c = ApiClient("https://api.example.com", token="t0k", transport=fake)
print(c.get("/users/1"))     # {'id': 1, 'name': 'ada'}

Centralizing base URL, auth and timeout means every call is consistent and you change reliability policy in one place.

Exercise 6 · Standardize reliability across servicesIndustry scenario

Context: Ten services each hand-rolling HTTP calls with inconsistent timeouts and no retries is how cascading outages start. As tech lead you ship one blessed way to make a call so reliability is enforced by code, not by a wiki page.

Your task: Ten services hand-roll their own HTTP with inconsistent timeouts and no retries. Define the org standard and show how a shared, configured session enforces it everywhere.

Requirements:

  • Ship a single factory (e.g. make_session) teams import instead of building bare sessions
  • Mount a Retry/HTTPAdapter that retries on 429/5xx with backoff
  • Enforce a default timeout on every outbound call
  • Retry only idempotent methods; do not auto-retry POSTs (double-write risk)
  • Make the point that a shared library — not documentation — is what actually enforces the standard

💡 Hint: Wrap the session's request to inject a default timeout, and scope retries to idempotent methods so a retried non-idempotent write can't silently double-apply.

Show solution

Ship one blessed factory that bakes in the org defaults; teams import it instead of constructing bare sessions.

import requests
from requests.adapters import HTTPAdapter
try:
    from urllib3.util.retry import Retry
except ImportError:                     # very old urllib3
    from requests.packages.urllib3.util.retry import Retry

def make_session(total=3, backoff=0.5, timeout=5):
    s = requests.Session()
    retry = Retry(total=total, backoff_factor=backoff,
                  status_forcelist=(429, 500, 502, 503, 504),
                  allowed_methods=frozenset(["GET", "PUT", "DELETE"]))
    adapter = HTTPAdapter(max_retries=retry)
    s.mount("https://", adapter)
    s.mount("http://", adapter)
    s.request = _with_default_timeout(s.request, timeout)  # enforce timeout
    return s

def _with_default_timeout(fn, timeout):
    def wrapper(method, url, **kw):
        kw.setdefault("timeout", timeout)
        return fn(method, url, **kw)
    return wrapper

The standard: every outbound call has a timeout; idempotent methods retry with backoff on 429/5xx; non-idempotent POSTs do not auto-retry (risk of double-writes). Enforcing it through a shared library — not a wiki page — is what actually stops the next cascade.

✓ Checkpoint — you can move on when you can…

  • Trace DNS→TCP→TLS→HTTP; use methods/status codes/JSON.
  • Handle statuses, retries+backoff, rate limits, and pagination.
  • Build a testable client with a fake session.
  • Standardize a reliable client for the whole team.

Knowledge check check yourself

✓ Knowledge check

What four steps happen between typing a URL and getting a response, in order?

Show answer
DNS resolves the name to an IP address, a TCP connection opens (with a TLS handshake for https), then an HTTP request is sent and an HTTP response (status + headers + body) comes back.
✓ Knowledge check

In a resilient client, why use exponential backoff on retries, and which failures should you actually retry?

Show answer
Exponential backoff doubles the wait between attempts (e.g. 0.01s, 0.02s, 0.04s, plus jitter) so you don't hammer a struggling server; you retry only transient/retryable errors — timeouts, 5xx, and 429 — not a client error like 400, and you cap the number of attempts.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in