Skip to main content

Module 11 — Recap and exam

The service that started life as a ten-line predict call in module 1 now behaves like a piece of infrastructure. It validates its inputs, loads a versioned model once at startup, serves single and batch predictions from one code path, distinguishes 400 from 422 from 500, offloads long jobs, gates access with a rotated token, emits structured logs and two health probes, ships as a small non-root container, and holds a documented load target with a measured cost. What is left is to walk that path back once, in one page, and to sit the exam that closes the course.

What each module contributed

  • Module 1 turned a Python function into a typed HTTP route with generated documentation.
  • Module 2 replaced hand-written validation with Pydantic and made 422 errors informative.
  • Module 3 moved the model load to application startup — one load per worker, not one per request.
  • Module 4 added a batch route that shares its scoring function with the single-row route, so the two never disagree.
  • Module 5 mapped every failure to the right status code and stopped Python tracebacks from leaking to callers.
  • Module 6 clarified when async helps (fan-out I/O) and when it hurts (a blocking model in the event loop), and offloaded file scoring to a background task.
  • Module 7 protected every non-meta route with an API key header, previewed JWT, and rotated keys without downtime.
  • Module 8 wrapped every request in a JSON log line with a request ID, and split /healthz from /ready so the orchestrator makes the right decision.
  • Module 9 built the container as a multi-stage, non-root, hash-locked image and picked a worker count.
  • Module 10 replaced guesswork with numbers: Locust scenario, p95 budget, capacity per replica, cost per month.

Every step layered on the previous ones. Nothing was thrown away.

The production checklist

Copy this list into the pull request that ships a new model API. If a box is not ticked, the reason belongs in the description.

  • Schemas — Every request and response is a Pydantic model with typed fields and, where relevant, constraints (gt, max_length, enums). The generated docs are readable.
  • Model loading — The model loads once at startup with a version identifier that the service exposes on /ready and on every prediction response. A failed load stops the process; it never falls through to a first-request timeout.
  • Preprocessing parity — The exact code that preprocessed training features is the code that preprocesses live features. Either the pipeline is baked into the artifact, or a shared package is imported by both sides.
  • Errors — 400 for business rules, 422 for schema, 500 for unexpected, 503 with Retry-After for warm-up and drain. A single JSON error shape with code, message, request_id. No traceback in a 500 body, ever.
  • Auth — The key travels in a header, never in a URL. Keys are hashed at rest, rotated on a schedule, and revocable in seconds without a redeploy. Rate limiting is on by default.
  • Observability — JSON logs with request_id, method, path, status, latency_ms. Liveness distinct from readiness. Metrics scrape (Prometheus or an aggregator query) tracks p95 and 5xx rate.
  • Container — Multi-stage build, slim base, non-root user, pinned hashes, image scanner in CI, no secret baked in.
  • Load & cost — A Locust scenario in the repo, a p95 budget documented in the README, a measured capacity per replica, and a monthly cost figure attached to the sizing decision.

Nine bullets, and every one of them was earned by an incident somewhere.

A one-shot smoke test that touches every module

Run this script against a freshly deployed replica before opening it to traffic. It exercises the routes from modules 1 through 8 in less than a second and prints a green line per contract.

# scripts/smoke.py
"""Post-deploy smoke test for the churn API."""
import os
import sys
import time
import uuid
import urllib.request
import json


BASE = os.environ["CHURN_BASE_URL"] # e.g. https://churn.internal
KEY = os.environ["CHURN_API_KEY"]


def _get(path: str) -> tuple[int, dict]:
req = urllib.request.Request(f"{BASE}{path}")
with urllib.request.urlopen(req, timeout=5) as resp:
return resp.status, json.loads(resp.read())


def _post(path: str, body: dict, key: str | None) -> tuple[int, dict]:
data = json.dumps(body).encode("utf-8")
headers = {"content-type": "application/json"}
if key:
headers["X-API-Key"] = key
req = urllib.request.Request(f"{BASE}{path}", data=data, headers=headers)
try:
with urllib.request.urlopen(req, timeout=5) as resp:
return resp.status, json.loads(resp.read())
except urllib.error.HTTPError as exc:
return exc.code, json.loads(exc.read())


def _check(label: str, ok: bool) -> None:
prefix = "ok " if ok else "FAIL "
print(prefix + label)
if not ok:
sys.exit(1)


def main() -> None:
# Module 8 — probes
_check("healthz is 200", _get("/healthz")[0] == 200)
ready_status, ready_body = _get("/ready")
_check("ready is 200 and exposes a model version",
ready_status == 200 and "model_version" in ready_body)

# Module 7 — auth
unauth_status, _ = _post("/predict", {}, key=None)
_check("predict without key is 401", unauth_status == 401)

# Modules 4 and 5 — single prediction and 422 shape
valid = {
"subscriber_id": f"smoke-{uuid.uuid4().hex[:8]}",
"tenure_months": 12,
"monthly_charges": 75.5,
"total_charges": 900.0,
"contract_type": "month_to_month",
"payment_method": "credit_card",
"is_senior": False,
}
status, body = _post("/predict", valid, key=KEY)
_check("predict returns 200 and a probability",
status == 200 and 0.0 <= body["churn_probability"] <= 1.0)

bad = dict(valid, tenure_months=-1)
status, _ = _post("/predict", bad, key=KEY)
_check("negative tenure is 422", status == 422)

# Module 4 — batch route
batch = {"items": [valid for _ in range(3)]}
status, body = _post("/predict/batch", batch, key=KEY)
_check("batch returns 200 and aligned items",
status == 200 and len(body["items"]) == 3)

print(f"smoke passed at {time.strftime('%H:%M:%S')}")


if __name__ == "__main__":
main()

Wire this script to the last step of the deploy pipeline. A green run is the signal that the new revision is ready for traffic; a red run rolls back automatically. It is the cheapest, most valuable safety net a model API can carry.

The 40-question exam

The course closes with a 40-question exam drawn from a bank of 48 items. The exam covers every module — from the shape of a Pydantic error, through the semantics of /healthz versus /ready, to the reading of a Locust percentile table. Questions are situational rather than definitional: expect scenarios asking what to do, not glossary lookups.

The exam rules are the same as every InSkillML premium course:

  • Pass mark: 70 percent, which is 28 correct answers out of 40.
  • Retakes: up to five attempts per 24-hour window. The 48-question pool means a second attempt is not the same 40 questions as the first.
  • Time: untimed. A focused sitting takes 40 to 60 minutes.
  • Language: sit the exam in the language you took the course in; the pool is per language and every question was written natively.

On success, the platform issues a certificate of completion with a verifiable identifier. It states that you can design, build, secure, observe and size a FastAPI service that serves a machine learning model in production — the specific skill this course teaches.

Before you start the exam

Two habits pay off during the sitting:

  • Read every option before picking one. Distractors in this bank were built from real mistakes production teams make, and the second-best answer is often the one that would ship and quietly fail.
  • Prefer explanations you can justify in one sentence. A question with four plausible options usually has one whose reasoning you can rebuild from memory; that is almost always the right one.

If a topic feels shaky, revisit the module before opening the exam. The modules are short by design; a re-read of one page is fifteen minutes well spent.

In summary

  • The course walks a single service from a ten-line route to a hardened container with a measured load target — every module builds on the previous.
  • The production checklist (schemas, load, preprocessing parity, errors, auth, observability, container, load and cost) is the same checklist you can copy into any model API pull request.
  • A one-shot smoke test that hits every contract belongs in the deploy pipeline; a red smoke rolls the revision back before real traffic sees it.
  • The 40-question exam, pass mark 70 percent, closes the course. Success delivers the verifiable premium certificate.

Good luck. When the certificate is issued, the service in your repository looks the way a production model API is supposed to look.

Final exam

Ready to validate this course?

40 questions drawn at random from the course bank · passing score 70% · verifiable PDF certificate issued immediately on success.

Start the exam

You need to be signed in to your InSkillML account with an active subscription. You can also start the exam from My courses.