AIDay 90 of 100Production
Capstone R5, Part 2
Launch & Prove It
"It works" isn't a launch. A hiring manager or an Orbit maintainer will ask five questions: how fast, how much per request, what if the provider goes down, how good, and what still fails? Today you answer each one with a number you measured, then ship R5 to production behind the gates you built.
Short on time? The 30-second version
- A launch is proven by numbers. Start on paper: tokens per request × rps × 60, minus cache hits, against your TPM limit, with the limiter at about 80%.
- Generate load open-loop and time from the scheduled send. Hunt your own bottlenecks keylessly (Stage A), then take a small, capped look at the real model (Stage B).
- A chaos drill measures what users saw (availability, fallback and degraded shares) and what you did (detect, mitigate, recover). In R5 an outage shows up as fallback share and cost, not errors.
- SLOs are targets for request-level SLIs with an error budget, set from measurements plus headroom. Quality and cost are SLOs too, and no data is not 100%.
- Ship from a tag through full evals, an approval, a smoke test and a canary gate, with a rollback you've run once, and lead the README with numbers and known failures.
At a Glance
- Time: ~3 h core (capstone; fine to split across two sittings) (+ optional Go Deeper) · Depth: working
- Prerequisites: Day 89: Capstone R5, Part 1: Integrate & Harden (your
r5.0.0-rc1on staging); the release reuses Day 47: Evals in CI, Day 59: Evaluating Agents and Day 75: Red-Teaming & Security Tests in CI - You'll build: R5 in production: a load-test harness, a chaos drill, SLOs, a $/request report, an eval-gated release with canary and rollback, a public demo, and a numbers-first README tagged
r5.0.0 - Stops at: multi-service scale (autoscaling and formal capacity planning are out of scope)
- Fast path: none for the finale; short on time, do Capacity on Paper, Stage B, the Chaos Drill, the Production Release and the Launch Report (~75 min)
- Cost: ≈ $1–5 in total, with Stage B hard-capped at $0.50 (prices as of 2026-11, verify). Without a key, Stage A, the drill and all tests run on
ATLAS_BACKEND=stub - New terms: TPM capacity planning, open-loop load test, SLO + error budget, launch report, eval-gated production release
Proving It With Numbers
Before a restaurant opens, the staff run a practice night (load test) and a fire drill (chaos), print promises and prices on the menu (SLOs, cost), then seat a few tables first (canary). R5 does the same:
| Question | Answered by | Section |
|---|---|---|
| How fast, up to what load? | load_test → LoadReport |
Load test, Stages A and B |
| What if the provider goes down? | run_chaos_drill |
Chaos Drill |
| What do we promise? | SLOS, slo_report() |
SLOs and Error Budgets |
| What does a request cost? | cost_per_request() |
Cost per Request |
| How good is it, what still fails? | full evals, known failures | Production Release, Launch Report |
Start state: your r5.0.0-rc1 on staging (or the course's r5-part1-reference tag). Today adds:
scripts/load_test.py NEW capacity math + open-loop generator, LoadReport (Day 93 reuses it)
atlas/chaos.py NEW a staging-only fault switch for the pools
scripts/chaos_drill.py NEW provider-outage drill on staging
atlas/slo.py NEW SLO, SLOS, slo_report(), cost_per_request()
scripts/release_gate.py NEW canary replay + Day 87's canary_gate()
scripts/launch_report.py NEW results/*.json -> README numbers block; eval floors
tests/test_slo.py NEW 5 tests
.github/workflows/release.yml, eval_thresholds.toml, LAUNCH_REPORT.md NEW (non-Python)
Imported, not re-printed: Day 86's replay_traffic and monitoring, Day 87's flags, Day 88's ops and game_day, Day 84's resilience, Day 83's auth. Day 89's assemble_r5 writes the carried files, including scripts/game_day.py.
Capacity on Paper
Start with arithmetic. TPM capacity planning turns a target load into the tokens per minute (TPM) the provider must accept, then checks it against your limit. Requests per minute (RPM) is the other limit, and you hit whichever runs out first.
By hand: Day 89's replay showed an ask is roughly 3,500 input and 400 output tokens. At 2 requests/s:
- (3,500 + 400) × 2 × 60 = 468,000 TPM
- With 30% cache hits (Day 85), only misses reach the model: 327,600 TPM
- Set Day 84's TPM bucket to about 80% of your limit, so bursts wait inside Atlas instead of earning 429s
Read your organization's real limit from its limits page or the x-ratelimit-limit-tokens header (Day 84). Don't assume one, and check the fallback model's limit too: an outage sends everything there.
The first block of load_test.py encodes the same math:
# script_id: day_090_capstone_r5_launch/scripts/load_test
# scripts/load_test.py: capacity math + an open-loop load generator for any Atlas-shaped API.
# Install: uv add httpx2 fastapi (httpx2 also arrives with openai>=3) Env: ATLAS_LOAD_KEYS, ORBIT_DATA
# Run: uv run python -m scripts.load_test --url https://staging.example --rps 0.2,0.5,1,2 --max-usd 0.50
# No --url: an offline demo against a toy in-process server. Day 93 reuses this file for self-hosted models.
from __future__ import annotations
import argparse
import asyncio
import json
import math
import os
import random
import time
from dataclasses import asdict, dataclass
from pathlib import Path
import httpx2
def tpm_needed(input_tokens: int, output_tokens: int, rps: float, cache_hit_rate: float = 0.0) -> int:
"""Tokens per minute the provider must accept. Cache hits never reach the model."""
return math.ceil((input_tokens + output_tokens) * rps * 60 * (1 - cache_hit_rate))
def limiter_rate(tpm_limit: int, headroom: float = 0.8) -> float:
"""Tokens per second for Day 84's TPM TokenBucket: aim at 80% of the limit, not 100%."""
return tpm_limit * headroom / 60
def pct(xs: list[float], q: float) -> float:
"""Nearest rank, the same rule as Day 86's pct(). Local copy: this file runs alone on the load box."""
s = sorted(xs)
return round(s[max(0, math.ceil(q / 100 * len(s)) - 1)], 1) if s else math.nan
A Load Test That Doesn't Lie
A naive generator runs 8 virtual users that each wait for a reply before sending again: a closed loop. When the server slows, they send less, so the test reports a calm p95 while real users queue. That blind spot is coordinated omission.
An open-loop load test draws the arrival schedule in advance (Poisson: random gaps averaging 1/rps) and sends on schedule regardless, timing each request from its scheduled send. The knee is the load where p95 starts rising fast: capacity is reached and arrivals queue.
LoadReport keeps three failure kinds apart:
| Column | Meaning | Owner |
|---|---|---|
error_rate |
5xx or a dropped connection | you, now |
own_429_rate |
Atlas's limiter or quota said no (Day 83) | a plan setting |
provider_429_rate |
provider pushback Atlas absorbed with a fallback or degraded answer (Day 84) | capacity |
# script_id: day_090_capstone_r5_launch/scripts/load_test
@dataclass
class LoadReport: # field names are stable: Day 93 and launch_report.py read them
rps_target: float
rps_achieved: float
n: int
p50_ms: float
p95_ms: float
p99_ms: float
ttft_p95_ms: float
error_rate: float # 5xx and transport errors: the outage column
own_429_rate: float # Atlas's limiter or quota said no (Day 83): a setting, not an outage
provider_429_rate: float # the provider pushed back and Atlas absorbed it (fallback/degraded, Day 84)
tokens_per_min: float # real usage from response bodies; cache hits add 0
cost_usd: float
aborted: int = 0 # arrivals never sent because the cost cap tripped
@dataclass
class Shot: # one request's outcome
latency_ms: float
status: int
ttft_ms: float | None = None
tokens: int = 0
cost: float = 0.0
absorbed: bool = False
@dataclass
class Spend: # shared by every step, so --max-usd caps the whole run
usd: float = 0.0
One request; streams also time the first token event (time to first token, TTFT):
# script_id: day_090_capstone_r5_launch/scripts/load_test
async def shoot(client: httpx2.AsyncClient, path: str, body: dict, key: str, scheduled: float) -> Shot:
"""Time from the SCHEDULED send, not the actual one: if the generator fell behind, a user still waited."""
headers, first, data = {"Authorization": f"Bearer {key}"}, None, {}
try:
if path.endswith("/stream"):
async with client.stream("POST", path, json=body, headers=headers) as r:
status, event = r.status_code, ""
async for line in r.aiter_lines():
if line.startswith("event: "):
event = line.removeprefix("event: ")
first = first or (time.perf_counter() if event == "token" else None)
elif line.startswith("data: ") and event == "final":
data = json.loads(line.removeprefix("data: "))
else:
r = await client.post(path, json=body, headers=headers)
status = r.status_code
data = r.json() if r.headers.get("content-type", "").startswith("application/json") else {}
except httpx2.TransportError: # refused, reset, timed out
status = 599
usage = data.get("usage") or {}
return Shot((time.perf_counter() - scheduled) * 1000, status,
ttft_ms=(first - scheduled) * 1000 if first else None,
tokens=usage.get("input_tokens", 0) + usage.get("output_tokens", 0),
cost=usage.get("cost_usd", 0.0), absorbed=bool(data.get("fallback") or data.get("degraded")))
One step never waits for replies before the next send:
# script_id: day_090_capstone_r5_launch/scripts/load_test
def summarize(rps: float, shots: list[Shot], elapsed_s: float, aborted: int) -> LoadReport:
n, ok = len(shots), [s for s in shots if s.status == 200] # latency of answers, not of fast 429s
share = lambda test: round(sum(map(test, shots)) / n, 4) if n else math.nan
lat = [s.latency_ms for s in ok]
return LoadReport(rps, round(n / elapsed_s, 2), n, pct(lat, 50), pct(lat, 95), pct(lat, 99),
pct([s.ttft_ms for s in ok if s.ttft_ms is not None], 95),
share(lambda s: s.status >= 500), share(lambda s: s.status == 429),
share(lambda s: s.absorbed), round(sum(s.tokens for s in shots) * 60 / elapsed_s),
round(sum(s.cost for s in shots), 4), aborted)
async def run_step(client: httpx2.AsyncClient, plan: list[tuple[str, dict, str]], rps: float, step_s: int,
spend: Spend, max_usd: float, rng: random.Random) -> LoadReport:
"""Open loop: arrival times are drawn up front (Poisson, mean gap 1/rps) and never wait for replies."""
start, t, tasks, aborted = time.perf_counter(), 0.0, [], 0
for path, body, key in plan:
t += rng.expovariate(rps)
if t >= step_s:
break
if spend.usd >= max_usd: # checked on finished requests: leave headroom
aborted += 1
continue
await asyncio.sleep(max(0.0, start + t - time.perf_counter()))
task = asyncio.create_task(shoot(client, path, body, key, start + t))
task.add_done_callback(lambda f: setattr(spend, "usd", spend.usd + f.result().cost))
tasks.append(task)
shots = await asyncio.gather(*tasks)
return summarize(rps, list(shots), time.perf_counter() - start, aborted)
One step per rate, questions from Day 86's Orbit mix, stopping at max_usd:
# script_id: day_090_capstone_r5_launch/scripts/load_test
MIX = [("/v1/ask", 0.85), ("/v1/ask/stream", 0.15)] # ask-heavy like Day 86's replay; streams measure TTFT
async def load_test(base_url: str, *, keys: dict[str, str], rps_steps: list[float], step_s: int = 60,
max_usd: float = 0.5, seed: int = 0, questions: list[str] | None = None,
mix: list[tuple[str, float]] = MIX,
transport: httpx2.AsyncBaseTransport | None = None) -> list[LoadReport]:
"""One LoadReport per step. Stops early once real spend reaches max_usd."""
if questions is None:
from scripts.replay_traffic import build_workload # Day 86's seeded Orbit mix (needs ORBIT_DATA)
questions = build_workload(500, seed)
rng, spend, reports, tenants = random.Random(seed), Spend(), [], sorted(keys)
limits = httpx2.Limits(max_connections=2_000, max_keepalive_connections=200) # never queue client-side
async with httpx2.AsyncClient(base_url=base_url, timeout=60.0, limits=limits, transport=transport) as client:
for rps in rps_steps:
plan = [(rng.choices([p for p, _ in mix], [w for _, w in mix])[0],
{"question": rng.choice(questions)}, keys[rng.choice(tenants)])
for _ in range(int(rps * step_s * 2) + 10)]
reports.append(await run_step(client, plan, rps, step_s, spend, max_usd, rng))
if spend.usd >= max_usd:
print(f"cost cap reached: ${spend.usd:.2f} of ${max_usd:.2f}; later steps skipped")
break
return reports
A table, and a toy server whose capacity you know in advance: 4 slots × 50 ms = 80 requests/s.
# script_id: day_090_capstone_r5_launch/scripts/load_test
def print_table(reports: list[LoadReport]) -> None:
print(f"{'rps':>5} {'got':>6} {'n':>5} {'p50 ms':>7} {'p95 ms':>7} {'ttft95':>7} {'5xx':>5} "
f"{'own429':>6} {'absorb':>6} {'TPM':>9} {'$':>7}")
for r in reports:
print(f"{r.rps_target:5g} {r.rps_achieved:6.1f} {r.n:5} {r.p50_ms:7.0f} {r.p95_ms:7.0f} {r.ttft_p95_ms:7.0f} "
f"{r.error_rate:5.1%} {r.own_429_rate:6.1%} {r.provider_429_rate:6.1%} {r.tokens_per_min:9,.0f} "
f"{r.cost_usd:7.4f}")
def toy_api(slots: int = 4, service_s: float = 0.05):
"""Offline stand-in that serves `slots` requests at a time: capacity = 4 / 0.05 s = 80 requests/s."""
from fastapi import FastAPI
app, gate = FastAPI(), asyncio.Semaphore(slots)
@app.post("/v1/ask")
async def ask(body: dict) -> dict:
async with gate: # arrivals beyond capacity wait here, like a busy worker
await asyncio.sleep(service_s)
return {"usage": {"input_tokens": 3_500, "output_tokens": 400, "cost_usd": 0.00055}}
return app
# script_id: day_090_capstone_r5_launch/scripts/load_test
if __name__ == "__main__":
ap = argparse.ArgumentParser(description="Open-loop load test for the Atlas API")
ap.add_argument("--url", help="API base URL; omit for the offline toy demo")
ap.add_argument("--rps", default="0.2,0.5,1,2", help="comma-separated arrival rates, one step each")
ap.add_argument("--step-s", type=int, default=60)
ap.add_argument("--max-usd", type=float, default=0.50)
ap.add_argument("--out", default="results/load.json")
a = ap.parse_args()
print(f"needed at 2 rps: {tpm_needed(3_500, 400, 2):,} TPM; with 30% cache hits: "
f"{tpm_needed(3_500, 400, 2, 0.3):,}; bucket at 80% of an example 500,000 TPM limit: "
f"{limiter_rate(500_000):,.0f} tokens/s")
if a.url:
keys = json.loads(os.environ["ATLAS_LOAD_KEYS"]) # {"loadtest": "atk_..."}: never in argv
reports = asyncio.run(load_test(a.url, keys=keys, rps_steps=[float(x) for x in a.rps.split(",")],
step_s=a.step_s, max_usd=a.max_usd))
else:
toy = httpx2.ASGITransport(app=toy_api()) # same process: fine for a demo, never for a test
reports = asyncio.run(load_test("http://toy", keys={"demo": "k"}, rps_steps=[20, 60, 100], step_s=3,
max_usd=5.0, questions=["How do retries back off?"],
mix=[("/v1/ask", 1.0)], transport=toy))
print_table(reports)
Path(a.out).parent.mkdir(parents=True, exist_ok=True)
Path(a.out).write_text(json.dumps([asdict(r) for r in reports], indent=1))
Offline demo, python -m scripts.load_test (the 500,000 TPM limit is illustrative; your numbers will differ):
needed at 2 rps: 468,000 TPM; with 30% cache hits: 327,600; bucket at 80% of an example 500,000 TPM limit: 6,667 tokens/s
rps got n p50 ms p95 ms ttft95 5xx own429 absorb TPM $
20 20.0 61 54 54 nan 0.0% 0.0% 0.0% 4,686,187 0.0335
60 58.7 179 54 77 nan 0.0% 0.0% 0.0% 13,730,809 0.0984
100 76.8 290 397 730 nan 0.0% 0.0% 0.0% 17,975,208 0.1595
At 20 and 60 requests/s p95 stays near the 50 ms service time. At 100 the toy is past capacity (it completes about 77/s) and p95 jumps more than tenfold: the knee.
Stage A: Find Your Own Bottlenecks for Free
Stage A loads staging on ATLAS_BACKEND=stub: the whole pipeline runs and no tokens are bought. Step from 5 to 50 requests/s; whatever breaks is yours.
The first run usually shows own429 near 100%: you measured the free plan's 60 requests/minute. Load tests need an internal plan:
PLANS: dict[Plan, PlanLimits] = { # atlas/quotas.py (Day 83)
"demo": PlanLimits(10, 50_000, 500, 300, 1, "none"),
"free": PlanLimits(60, 200_000, 2_000, 800, 2, "low"),
"team": PlanLimits(300, 2_000_000, 8_000, 2_000, 3, "medium"),
+ "internal": PlanLimits(6_000, 50_000_000, 2_000, 800, 50, "low"), # load tests + release gates only
}
Add "internal" to the Plan literal (atlas/api/auth.py) and issue a staging-only key with KeyStore.issue("loadtest", "internal", {"ask"}) into ATLAS_LOAD_KEYS.
uv run python -m scripts.load_test --url https://staging.example --rps 5,10,20,50 --step-s 60 \
--out results/load_stage_a.json
Then find one bottleneck, fix it, and re-run the same steps; Go Deeper walks through the likely one in Day 89's code.
Stage B: Real Model, Hard Cost Cap
Stage B switches staging to ATLAS_BACKEND=reference and buys tokens: 0.2 → 0.5 → 1 → 2 requests/s for 60 s each, at most about 220 asks. At ≈ $0.00055 per luna ask (3,500 × $0.10/1M + 400 × $0.50/1M, as of 2026-11, verify) that's about $0.12. The --max-usd 0.50 cap is for the surprise, a routing bug sending everything to the 10× model. Use a staging-only project key with its own spend limit.
uv run python -m scripts.load_test --url https://staging.example --rps 0.2,0.5,1,2 --max-usd 0.50 \
--out results/load_stage_b.json
Read it in order: 5xx must be 0; own429 0 on the internal plan; a rising absorb means you're near the provider's limit, so compare observed TPM with tpm_needed(). p95 and TTFT feed the SLOs below; $ / n is $/request under load.
Stage B (reference, gpt-6-luna) |
0.2 rps | 0.5 rps | 1 rps | 2 rps |
|---|---|---|---|---|
| p50 / p95 / p99 (ms) | ||||
| TTFT p95 (ms) | ||||
| 5xx / own 429 / absorbed | ||||
TPM observed vs tpm_needed() |
||||
| $ for the step |
Your numbers will differ; Exercise 2 measures what the cache buys.
Chaos Drill: The Provider Goes Down
Day 88 rehearsed on a simulated clock. Now take the primary model down on staging for 5 minutes while traffic flows, and time what users saw and what you did.
A drill needs a fault switch. Every model call goes through the client inside Day 89's pools, so ChaosClient wraps it and fails the models named in one field of Day 88's audited override hash.
# script_id: day_090_capstone_r5_launch/atlas/chaos
# atlas/chaos.py: a fault switch for staging drills. build_deps never installs it in prod.
# Stdlib + atlas. The switch is one field in Day 88's audited override hash, so drills leave a trail.
from __future__ import annotations
import time
from typing import Any
from atlas.ops import OVERRIDES_KEY, set_override
from atlas.resilience import FaultInjector
CHAOS_FIELD = "chaos_down" # comma-separated model ids that should fail like an outage
class ChaosClient:
"""Wraps any client with .responses.create; models named in the override fail with Day 84's fake 503."""
def __init__(self, inner: Any, redis: Any, *, max_age_s: float = 2.0) -> None:
self.inner, self.redis, self.max_age_s, self.responses = inner, redis, max_age_s, self
self._down: set[str] = set()
self._read_at = float("-inf")
def down(self) -> set[str]:
if time.monotonic() - self._read_at >= self.max_age_s: # one Redis read per 2 s, like Day 88's levers
try:
raw = self.redis.hgetall(OVERRIDES_KEY).get(CHAOS_FIELD, "")
self._down = {m for m in raw.split(",") if m}
except Exception: # a broken ops store never breaks requests
pass
self._read_at = time.monotonic()
return self._down
async def create(self, *, model: str, **kwargs: Any) -> Any:
if model in self.down():
raise FaultInjector({}, down={model}).pick(model) # retryable 503: breakers and fallbacks react
return await self.inner.responses.create(model=model, **kwargs)
def set_chaos(redis: Any, models: set[str], *, actor: str, reason: str) -> None:
"""An empty set turns the fault off. Audited, so the postmortem timeline has exact times."""
set_override(redis, CHAOS_FIELD, ",".join(sorted(models)), actor=actor, reason=reason)
Install it in Day 89's composition root, outside prod only:
+from atlas.chaos import ChaosClient
...
- pools, breakers = build_pools(ParseShim(client) if client else FakeAsyncClient(FaultInjector({})), models)
+ ops_redis = get_sync_redis(settings.redis_url)
+ inner = ParseShim(client) if client else FakeAsyncClient(FaultInjector({}))
+ if settings.env != "prod": # Day 90: staging can be told to fail
+ inner = ChaosClient(inner, ops_redis)
+ pools, breakers = build_pools(inner, models)
redis = get_redis(settings.redis_url)
settings.state_dir.mkdir(parents=True, exist_ok=True)
- return Deps(settings=settings, redis=redis, ops_redis=get_sync_redis(settings.redis_url),
+ return Deps(settings=settings, redis=redis, ops_redis=ops_redis,
Day 86's availability alert watches error_rate, but R5 falls back to gpt-6.1-terra, so errors stay near zero while each answer costs about 10× more. The drill pages on the fallback share instead.
# script_id: day_090_capstone_r5_launch/scripts/chaos_drill
# scripts/chaos_drill.py: take the primary model down on staging, replay traffic, time detection and recovery.
# Run ON the staging API host (it reads the API's event log), e.g.:
# docker compose exec -T api python -m scripts.chaos_drill --minutes 5
# Env: ATLAS_REDIS_URL, ATLAS_STATE_DIR, ATLAS_MODEL, ATLAS_DRILL_KEYS (JSON tenant -> key; not the demo key)
from __future__ import annotations
import json
import os
import sys
import threading
import time
from dataclasses import asdict, dataclass
from pathlib import Path
from atlas.chaos import set_chaos
from atlas.monitoring import RULES, AlertRule, EventLog, evaluate_alerts
from atlas.ops import get_redis, set_override
from scripts.game_day import DrillReport # Day 88: scenario, mttd_s, mttr_s, alerts, timeline
from scripts.replay_traffic import replay_traffic # Day 86
# New rule: R5's fallbacks keep error_rate near 0 in an outage, so page on the fallback share instead.
FALLBACK_SURGE = AlertRule("fallback_surge", "fallback", ">", 0.2, 5, 30, "page")
DRILL_RULES = [r for r in RULES if r.name in ("availability", "spend")] + [FALLBACK_SURGE]
@dataclass
class LaunchDrill(DrillReport):
n: int = 0 # responses during the fault window
availability: float = 0.0 # non-5xx share; degraded answers count as available
fallback_share: float = 0.0 # answered by a model other than the primary
degraded_share: float = 0.0 # cited links only, no generated answer (Day 84)
mitigated_s: float | None = None # fault on -> runbook lever applied
recovered_s: float | None = None # fault off -> first answer from the primary again
The loop follows Day 88's runbook: fault on after a baseline minute, alerts evaluated every 10 s, a scripted responder applying fallback force a minute after the page, and a finally that always turns the fault off.
# script_id: day_090_capstone_r5_launch/scripts/chaos_drill
def run_chaos_drill(base_url: str, *, keys: dict[str, str], minutes: float = 5, rps: float = 1.0,
redis=None, log: EventLog | None = None, down: set[str] | None = None,
lead_s: float = 60, tick_s: float = 10, responder_delay_s: float = 60,
send=None, workers: int = 8) -> LaunchDrill:
redis = redis or get_redis(os.environ["ATLAS_REDIS_URL"]) # unset would silently use a private in-process store
log = log or EventLog(Path(os.environ.get("ATLAS_STATE_DIR", ".atlas")) / "events.db")
primary = os.environ.get("ATLAS_MODEL", "gpt-6-luna")
down = down or {primary}
rep, seen, t0 = LaunchDrill("provider_outage"), [], time.time()
clock = lambda: round(time.time() - t0, 1)
note = lambda what: rep.timeline.append((clock(), what))
on_result = lambda q, tenant, user, status, body: seen.append((clock(), status, body))
replay = threading.Thread(target=replay_traffic, args=(base_url,), daemon=True, kwargs=dict(
keys=keys, n=int((2 * lead_s + minutes * 60) * rps), rps=rps, send=send, workers=workers,
on_result=on_result))
replay.start()
fault_on = fault_off = first_alert = None
# script_id: day_090_capstone_r5_launch/scripts/chaos_drill
# (run_chaos_drill continues: one pass every tick_s until the replay ends)
try:
while replay.is_alive():
t = clock()
if fault_on is None and t >= lead_s:
set_chaos(redis, down, actor="drill", reason="game day: provider outage")
fault_on = t
note(f"fault on: {', '.join(sorted(down))} return 503")
if fault_on is not None and fault_off is None and t >= fault_on + minutes * 60:
set_chaos(redis, set(), actor="drill", reason="game day: fault off")
set_override(redis, "force_fallback", "", actor="drill", reason="runbook: undo the lever")
fault_off = t
note("fault off; runbook: atlas_ops fallback auto")
firing = {a.rule for a in evaluate_alerts(log, DRILL_RULES)}
for name in sorted(firing - set(rep.alerts)):
rep.alerts.append(name)
note(f"ALERT {name}")
if fault_on is not None and rep.mttd_s is None:
rep.mttd_s, first_alert = round(t - fault_on, 1), t
if first_alert and rep.mitigated_s is None and fault_off is None and t - first_alert >= responder_delay_s:
set_override(redis, "force_fallback", "1", actor="drill", reason="runbook step 1: provider outage")
rep.mitigated_s = round(t - fault_on, 1)
note("mitigation: atlas_ops fallback force")
if rep.mitigated_s is not None and rep.mttr_s is None and not firing:
rep.mttr_s = round(t - fault_on, 1)
note("alerts clear")
time.sleep(tick_s)
finally: # a drill must never leave the fault on
set_chaos(redis, set(), actor="drill", reason="drill ended")
set_override(redis, "force_fallback", "", actor="drill", reason="drill ended")
return summarize_drill(rep, seen, fault_on, fault_off, primary)
# script_id: day_090_capstone_r5_launch/scripts/chaos_drill
def summarize_drill(rep: LaunchDrill, seen: list, fault_on: float | None, fault_off: float | None,
primary: str) -> LaunchDrill:
"""Availability and shares over the fault window; recovery = first primary answer after fault off."""
end = fault_off if fault_off is not None else float("inf")
window = [(s, b) for t, s, b in seen if fault_on is not None and fault_on <= t < end]
share = lambda test: round(sum(map(test, window)) / len(window), 4) if window else 0.0
rep.n = len(window)
rep.availability = share(lambda sb: sb[0] < 500)
rep.fallback_share = share(lambda sb: sb[0] == 200 and (bool(sb[1].get("fallback")) or sb[1].get("model") != primary))
rep.degraded_share = share(lambda sb: bool(sb[1].get("degraded")))
back = [t for t, s, b in seen if fault_off is not None and t >= fault_off and s == 200
and b.get("model") == primary and not b.get("fallback") and not b.get("cached")]
rep.recovered_s = round(back[0] - fault_off, 1) if back else None
return rep
if __name__ == "__main__":
minutes = float(sys.argv[sys.argv.index("--minutes") + 1]) if "--minutes" in sys.argv else 5.0
base = os.environ.get("ATLAS_API_URL", "http://localhost:8000")
rep = run_chaos_drill(base, keys=json.loads(os.environ["ATLAS_DRILL_KEYS"]), minutes=minutes)
print(f"availability {rep.availability:.2%} fallback {rep.fallback_share:.0%} "
f"degraded {rep.degraded_share:.0%} n={rep.n}")
print(f"detect {rep.mttd_s}s mitigate {rep.mitigated_s}s alerts clear {rep.mttr_s}s "
f"primary back {rep.recovered_s}s after fault off")
for t, what in rep.timeline:
print(f" t={t:>6}s {what}")
out = Path(os.environ.get("ATLAS_RESULTS_DIR", "results"))
out.mkdir(parents=True, exist_ok=True)
(out / "drill.json").write_text(json.dumps(asdict(rep), indent=1))
A scaled-down in-process run on the stub backend (a 1-minute fault, 2 requests/s, 2-s ticks, the alert window cut to 1 minute); your 7-minute staging run will differ:
availability 100.00% fallback 95% degraded 0% n=115
detect 8.0s mitigate 24.0s alerts clear 70.1s primary back 3.4s after fault off
t= 20.1s fault on: gpt-6-luna return 503
t= 28.1s ALERT fallback_surge
t= 44.1s mitigation: atlas_ops fallback force
t= 80.2s fault off; runbook: atlas_ops fallback auto
t= 90.3s alerts clear
Availability stays at 100% because fallbacks count as available; the fallback share shows what users got. fallback force answers carry no fallback=true though each costs about 10× more, so watch model and $/request too. Write the drill up with Day 88's postmortem() template and file the gaps.
SLOs and Error Budgets
A service-level indicator (SLI) measures one thing ("this ask took under 4 s"); a service-level objective (SLO) is its target (95% of asks over 30 days); the error budget is the failure it allows. At 99.5% over 30 days: 0.005 × 30 × 24 × 60 = 216 minutes (3.6 h) of full outage. Budget left: ship. Spent: reliability first.
Every Atlas SLI judges one event row as good, bad or not counted, so one formula covers all five: actual = good / counted and budget_left = 1 − (1 − actual) / (1 − target) (1 = untouched, 0 = spent, negative = missed).
| SLO | Good when… | Target |
|---|---|---|
| availability | status < 500 (degraded counts; reported separately) | 99.5% |
| ask_latency | a 200 ask took ≤ 4,000 ms | 95% |
| ttft | first token ≤ 1,500 ms | 95% |
| quality | Day 86's sampled online checks passed | 95% |
| ask_cost | a 200 ask cost ≤ $0.002 | 95% |
Thresholds are placeholders: set each from Stage B's p95 × ~1.5, not from a blog post, and revisit after a week. The cost line sits between a luna ask (≈ $0.00055) and a terra ask (≈ $0.0055), so fallback-heavy days burn it.
# script_id: day_090_capstone_r5_launch/atlas/slo
# atlas/slo.py: request-based SLOs over Day 86's event log, error budgets, and $/request by route.
# Stdlib + atlas. Run: uv run python -m atlas.slo --days 7 > results/slo.json (reads $ATLAS_STATE_DIR/events.db)
from __future__ import annotations
import json
import math
import os
import sys
from collections.abc import Callable
from dataclasses import asdict, dataclass
from pathlib import Path
from atlas.monitoring import EventLog, RequestEvent, pct
Judge = Callable[[RequestEvent, "bool | None"], "bool | None"] # (event, checks passed?) -> good? None = skip
@dataclass(frozen=True)
class SLO:
name: str
sli: str # a key of SLIS
target: float # share of counted requests that must be good
window_days: int = 30
@dataclass(frozen=True)
class SLOResult:
name: str
target: float
actual: float
budget_left: float # 1.0 untouched, 0 spent, < 0 missed
n: int = 0
# Thresholds are PLACEHOLDERS: Stage B p95 x ~1.5, then revisit after a week of real traffic.
SLIS: dict[str, Judge] = {
"non_5xx": lambda e, ok: e.status < 500, # 429s and degraded 200s are the plan working
"ask_under_4s": lambda e, ok: e.latency_ms <= 4_000 if e.route == "ask" and e.status == 200 else None,
"ttft_under_1500ms": lambda e, ok: e.ttft_ms <= 1_500 if e.ttft_ms is not None else None,
"online_checks_pass": lambda e, ok: ok, # None unless Day 86 sampled the request
"ask_under_0_002_usd": lambda e, ok: e.cost_usd <= 0.002 if e.route == "ask" and e.status == 200 else None,
}
SLOS = [SLO("availability", "non_5xx", 0.995), SLO("ask_latency", "ask_under_4s", 0.95),
SLO("ttft", "ttft_under_1500ms", 0.95), SLO("quality", "online_checks_pass", 0.95),
SLO("ask_cost", "ask_under_0_002_usd", 0.95)]
# script_id: day_090_capstone_r5_launch/atlas/slo
def checks_by_request(log: EventLog, events: list[RequestEvent]) -> dict[str, bool]:
"""A request passes only if every code check on it passed (the same rule as Day 86's metrics())."""
passed: dict[str, bool] = {}
for s in log.scores(events):
if s.source == "code":
passed[s.request_id] = passed.get(s.request_id, True) and s.passed
return passed
def slo_report(log: EventLog, window_days: int, slos: list[SLO] = SLOS) -> list[SLOResult]:
events = log.window(window_days * 24 * 60)
passed, out = checks_by_request(log, events), []
for slo in slos:
verdicts = [v for e in events if (v := SLIS[slo.sli](e, passed.get(e.request_id))) is not None]
actual = sum(verdicts) / len(verdicts) if verdicts else math.nan # no data is NOT 100%
budget = 1 - (1 - actual) / (1 - slo.target)
out.append(SLOResult(slo.name, slo.target, round(actual, 4), round(budget, 3), len(verdicts)))
return out
def budget_minutes(slo: SLO) -> float:
"""The time reading of a budget: 99.5% over 30 days allows 216 minutes of full outage."""
return (1 - slo.target) * slo.window_days * 24 * 60
Fill this from your staging window:
| SLO | Target | Actual (staging, 7 days) | Budget left | n |
|---|---|---|---|---|
| availability | 99.5% | |||
| ask_latency | 95% | |||
| ttft | 95% | |||
| quality | 95% | |||
| ask_cost | 95% |
Expect ttft to read n/a with n = 0: Day 89's stream never writes ttft_ms, so that SLO has no SLI behind it. One line fixes it (until token streaming lands, TTFT equals answer time; say so in the README):
+ t.ev.ttft_ms = round((time.perf_counter() - t.started) * 1000, 1) # Day 90: the TTFT SLI
yield AgentEvent(type="token", data={"text": r.answer.text}) # one chunk: see Go Deeper
Cost per Request, Honestly
cost_per_request() reports p50/p95 per route from real usage (Day 89 prices every event). Cache hits stay in at $0, with their savings attributed; by="model" shows Day 85's routing split.
# script_id: day_090_capstone_r5_launch/atlas/slo
def cost_per_request(log: EventLog, by: str = "route", window_days: int = 30) -> dict[str, dict[str, float]]:
"""$/request from real usage, per route (or model, or tenant_id). Cache hits count, at $0."""
groups: dict[str, list[RequestEvent]] = {}
for e in log.window(window_days * 24 * 60):
if e.status == 200:
groups.setdefault(str(getattr(e, by)), []).append(e)
out = {}
for key, evs in sorted(groups.items()):
costs, misses = [e.cost_usd for e in evs], [e.cost_usd for e in evs if not e.cached]
hits = sum(bool(e.cached) for e in evs)
out[key] = {"n": len(evs), "mean": sum(costs) / len(evs), "p50": pct(costs, 50), "p95": pct(costs, 95),
"p95_ms": pct([e.latency_ms for e in evs], 95), "cache_hit": hits / len(evs),
"saved_usd": hits * (sum(misses) / len(misses) if misses else 0.0)} # Day 85's lever, priced
return out
if __name__ == "__main__":
days = int(sys.argv[sys.argv.index("--days") + 1]) if "--days" in sys.argv else 7
log = EventLog(Path(os.environ.get("ATLAS_STATE_DIR", ".atlas")) / "events.db")
print(json.dumps({"slos": [asdict(r) for r in slo_report(log, days)],
"cost": cost_per_request(log, window_days=days)}, indent=1))
A monthly projection is arithmetic on stated volume: 3,500 asks/day × 30 × $0.0004 mean = $42/month (illustrative volumes; prices as of 2026-11, verify). Price investigate separately: one run reserves up to 20,000 tokens.
Tests on a hand-made log: 200 events, 2 of them 5xx, 10 slow, 50 cache hits.
# script_id: day_090_capstone_r5_launch/tests/test_slo
# tests/test_slo.py: SLO and cost math on a hand-made event log. No key, no services.
# Run: uv run pytest tests/test_slo.py -q
from __future__ import annotations
import math
import time
import pytest
from atlas.monitoring import EventLog, RequestEvent
from atlas.slo import SLOS, budget_minutes, cost_per_request, slo_report
from scripts.load_test import tpm_needed
def ev(i: int, *, status: int = 200, ms: float = 900.0, cached: bool = False) -> RequestEvent:
return RequestEvent(request_id=f"r{i}", ts=time.time() - 60, tenant_id="acme", route="ask",
model="gpt-6-luna", status=status, latency_ms=ms,
cost_usd=0.0 if cached else 0.0005, cached=cached)
@pytest.fixture
def log(tmp_path) -> EventLog:
log = EventLog(tmp_path / "events.db")
for i in range(200): # r0, r1 are 5xx; every 20th is slow; every 4th is a cache hit
log.record(ev(i, status=503 if i < 2 else 200, ms=6_000 if i % 20 == 0 else 900, cached=i % 4 == 0))
return log
def by_name(log: EventLog) -> dict:
return {r.name: r for r in slo_report(log, 1)}
# script_id: day_090_capstone_r5_launch/tests/test_slo
def test_1_tpm_by_hand():
assert tpm_needed(3_500, 400, 2) == 468_000
assert tpm_needed(3_500, 400, 2, cache_hit_rate=0.3) == 327_600
def test_2_availability_budget_is_overspent(log):
a = by_name(log)["availability"]
assert (a.actual, a.n) == (0.99, 200) # 2 of 200 failed
assert a.budget_left == -1.0 # 1 failure allowed, 2 used: missed
def test_3_latency_counts_only_successful_asks(log):
lat = by_name(log)["ask_latency"]
assert lat.n == 198 and lat.actual == pytest.approx(189 / 198, abs=1e-4) # 9 slow 200s
def test_4_no_data_is_nan_not_100_percent(log):
ttft = by_name(log)["ttft"]
assert ttft.n == 0 and math.isnan(ttft.actual)
assert budget_minutes(SLOS[0]) == pytest.approx(216.0)
def test_5_cache_hits_cost_zero_and_are_attributed(log):
c = cost_per_request(log)["ask"]
assert c["n"] == 198 and c["cache_hit"] == pytest.approx(49 / 198)
assert c["saved_usd"] == pytest.approx(49 * 0.0005) and c["p95"] == 0.0005
..... [100%]
5 passed
The Eval-Gated Production Release
An eval-gated production release can't reach users unless the full quality suites pass, and it can undo itself. It runs only on a release tag, so a merge never ships by accident:
- Keyless tests: unit, Day 89's integration tests,
test_slo. - Full evals: Day 47 in
fullmode, Day 59 live, Day 75's suite (offline, plus live if you built its adapter), then the floors ineval_thresholds.toml. - Build the image, tagged with the release tag.
- A required reviewer approves the
productionenvironment; deploy with Day 82'sdeploy.py. - Smoke test, then the canary gate: the release's behaviour change (
askprompt v3 → v4) serves 10% of keys whilerelease_gatereplays traffic through Day 87'scanary_gate(). - Any failure redeploys the last good image.
The floors are policy, not benchmarks: start from rc1 minus a margin. A missing metric fails, so a suite that didn't run can't pass.
# file: eval_thresholds.toml (launch floors; Day 47's PR thresholds stay where they are)
[floors] # "<suite>.<metric>" = minimum. PLACEHOLDERS: set from your rc1 results.
"retrieval.recall_at_5" = 0.80
"retrieval.mrr" = 0.60
"answers.faithfulness_pass" = 0.90
"triage.label_accuracy" = 0.80
"triage.duplicate_f1" = 0.70
"agent.trajectory_pass" = 0.85
"redteam.pass_rate" = 1.0
Each suite reports its headline numbers with one record_eval() call (from launch_report.py, below) at the end of its runner, for example record_eval(Path("results"), "redteam", "pass_rate", passed / total, total, "<dataset>@<version>").
# file: .github/workflows/release.yml (new; Day 82's deploy.yml keeps PRs and staging)
name: release
on:
push:
tags: ["r5.*"] # r5.0.0, r5.0.1, and rc tags like r5.0.0-rc2
permissions:
contents: read
defaults:
run:
shell: bash
concurrency:
group: release-prod # one production release at a time
cancel-in-progress: false
jobs:
test:
runs-on: ubuntu-latest
env: { ATLAS_BACKEND: stub, ATLAS_REDIS_URL: "fake://" }
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v10.3.3
with: { enable-cache: true }
- run: uv sync --locked --all-extras --dev
- run: uv run pytest -m "not eval and not live" -q # unit, integration (D89), test_slo
evals: # the FULL suites; PRs ran the fast tier
needs: test
runs-on: ubuntu-latest
timeout-minutes: 45
environment: release-evals # an eval-only OpenAI key with its own spend limit
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ATLAS_MODEL: gpt-6-luna
EVAL_SUITE: full
EVAL_OUT: evals
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v10.3.3
with: { enable-cache: true }
- run: uv sync --locked --all-extras --dev
- run: uv run pytest -m eval tests/test_eval_gate.py # Day 47: retrieval + answers
- run: uv run python atlas/evals/run_suite_cli.py --live # Day 59's runner (k=3) next to agent_evals.py
- run: uv run pytest tests/security # Day 75: offline cases
- run: uv run python tests/security/suite.py --out reports # add --target live_target:live_target --k 3 once built
- run: uv run python -m scripts.launch_report results --gate eval_thresholds.toml
- if: always()
run: cat results/evals.jsonl >> "$GITHUB_STEP_SUMMARY" # copy these lines into results/ for the README
build:
needs: evals
runs-on: ubuntu-latest
permissions: { contents: read, packages: write }
steps:
- uses: actions/checkout@v7
- run: |
IMAGE="ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/atlas:${GITHUB_REF_NAME}"
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
docker build --build-arg ATLAS_RELEASE="${GITHUB_REF_NAME}" -t "$IMAGE" .
docker push "$IMAGE"
deploy-prod:
needs: build
runs-on: [self-hosted, prod]
environment: production # required reviewers: a person approves every prod deploy
permissions: { contents: read, packages: read }
env:
ATLAS_ENV: prod
ATLAS_ENV_FILE: /etc/atlas/prod.env # ATLAS_RELEASE_FILE=/app/state/release.toml lives in here
ATLAS_RELEASES_FILE: /var/lib/atlas/releases
steps:
- uses: actions/checkout@v7
- run: |
echo "ATLAS_IMAGE=ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/atlas" >> "$GITHUB_ENV"
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
- run: python3 scripts/deploy.py deploy "$GITHUB_REF_NAME"
- run: python3 scripts/smoke_test.py http://localhost:8000
env: { ATLAS_SMOKE_KEY: "${{ secrets.ATLAS_SMOKE_KEY }}" }
- run: docker compose exec -T api python -m scripts.release_gate --replay 1000
- run: python3 scripts/deploy.py mark-good "$GITHUB_REF_NAME"
- if: failure() # smoke or gate failed: put the last good image back
run: python3 scripts/deploy.py rollback
Add the required-reviewer protection rule to production. The prod runner is a self-hosted runner on the prod host, as Day 82 set up for staging.
Day 89 buckets canaries by API key, so a replay with three tenant keys puts zero or one key in a 10% canary. release_gate issues 100 short-lived keys on an internal tenant, replays 1,000 asks, then revokes them.
# script_id: day_090_capstone_r5_launch/scripts/release_gate
# scripts/release_gate.py: after a prod deploy, replay traffic through the canary, then promote or roll back.
# Run on the prod host: docker compose exec -T api python -m scripts.release_gate --replay 1000
# Exit 0 = promote or hold (a hold keeps the canary %: re-run later), 1 = rollback (canary set to 0%). ~$0.50 on gpt-6-luna.
from __future__ import annotations
import math
import os
import re
import sys
import time
from pathlib import Path
from atlas.api.auth import KeyStore
from atlas.flags import GUARDS, VariantStats, canary_gate
from atlas.monitoring import EventLog, pct
from atlas.slo import checks_by_request
from scripts.replay_traffic import replay_traffic
STATE = Path(os.environ.get("ATLAS_STATE_DIR", ".atlas"))
RELEASE = Path(os.environ.get("ATLAS_RELEASE_FILE", "release.toml"))
def variant_stats(log: EventLog, minutes: int) -> dict[str, VariantStats]:
"""Day 87's VariantStats, computed from Day 86's event log (R5 writes `variant` on every row)."""
events = [e for e in log.window(minutes) if e.route == "ask"]
checks, out = checks_by_request(log, events), {}
rate = lambda xs: sum(xs) / len(xs) if xs else math.nan
for v in ("control", "canary"):
ev = [e for e in events if e.variant == v]
ok = [e for e in ev if e.status < 500]
out[v] = VariantStats(n=len(ev), pass_rate=rate([checks[e.request_id] for e in ev if e.request_id in checks]),
citation_rate=rate([e.citations > 0 for e in ok]),
p95_ms=pct([e.latency_ms for e in ok], 95),
cost_per_request=rate([e.cost_usd for e in ev]),
error_rate=rate([e.status >= 500 for e in ev]))
return out
# script_id: day_090_capstone_r5_launch/scripts/release_gate
def set_canary_pct(path: Path, value: int) -> None:
"""Built on Day 87's helper (one canary per file). ReleaseWatcher picks the edit up: no redeploy."""
path.write_text(re.sub(r"(?m)^pct = .*$", f"pct = {value}", path.read_text(), count=1))
def gate_keys(store: KeyStore, n: int = 100) -> dict[str, str]:
"""Canaries bucket by API key (Day 89), so the replay needs many keys. Revoked after the run."""
return {f"gate-{i:03}": store.issue("release-gate", "internal", {"ask"}) for i in range(n)}
if __name__ == "__main__":
n = int(sys.argv[sys.argv.index("--replay") + 1]) if "--replay" in sys.argv else 1000
store, start = KeyStore(str(STATE / "keys.db")), time.time()
keys = gate_keys(store)
try:
print(replay_traffic(os.environ.get("ATLAS_API_URL", "http://localhost:8000"), keys=keys, n=n, rps=2.0))
time.sleep(10) # let Day 86's background checks land
finally:
for raw in keys.values():
store.revoke(raw.split("_")[1]) # the key id, as seed_tenants prints it
minutes = math.ceil((time.time() - start) / 60)
decision = canary_gate(variant_stats(EventLog(STATE / "events.db"), minutes), GUARDS)
print(decision)
if decision.action != "hold":
set_canary_pct(RELEASE, 100 if decision.action == "promote" else 0)
sys.exit(1 if decision.action == "rollback" else 0)
Test the rollback once, on purpose: tag r5.0.0-rc2 with an ask v4 that drops citations, and time tag → ROLLBACK: citation_rate … → restored service. A rollback you've never run is a theory.
A Public Demo That Can't Bankrupt You
A public demo link is an open door to your bill. The demo tenant (Day 89) is ask-only on Day 83's tightest plan: 10 requests/minute, 50,000 tokens/day, 500-token questions. Prove it before you publish:
ATLAS_LOAD_KEYS='{"demo": "<demo key>"}' uv run python -m scripts.load_test --url https://atlas.example \
--rps 0.5 --step-s 60 --out results/demo_quota.json
Expect own429 near two-thirds and no 5xx; investigate with the demo key must return 403.
Keep the key server-side. On Hugging Face Spaces it goes in the Space's Secrets. Free accounts can host up to 2 Gradio Spaces on ZeroGPU, and CPU Gradio Spaces need a paid plan (as of 2026-11, see the Spaces overview; ⚠ verify).
Record a 2-minute demo: a cited answer, an investigation streaming its tool steps, a triage card awaiting approval, then Day 86's dashboard showing those requests.
The Launch Report and README
A launch report holds the evidence behind a release: method, raw tables, drill timeline, SLOs and known failures. The README leads with the headline numbers. launch_report.py builds the README block from results/.
# script_id: day_090_capstone_r5_launch/scripts/launch_report
# scripts/launch_report.py: results/*.json -> README's numbers-first block; eval floors for the release.
# Stdlib only. Run: python -m scripts.launch_report results > results/README_numbers.md
# --gate eval_thresholds.toml : exit 1 if any eval is below its floor (or missing)
# --sample : print a README from SAMPLE data (illustrative, not measured)
from __future__ import annotations
import argparse
import json
import math
import sys
import tempfile
import time
import tomllib
from pathlib import Path
def record_eval(results_dir: Path, suite: str, metric: str, value: float, n: int, dataset: str) -> None:
"""Call once per headline metric at the end of each eval runner (Days 47, 59, 75, 76)."""
results_dir.mkdir(parents=True, exist_ok=True)
with (results_dir / "evals.jsonl").open("a") as f:
f.write(json.dumps({"suite": suite, "metric": metric, "value": value, "n": n, "dataset": dataset}) + "\n")
def load(results_dir: Path, name: str, default):
p = results_dir / name
if not p.exists():
return default
if name.endswith(".jsonl"):
return [json.loads(line) for line in p.read_text().splitlines() if line.strip()]
return json.loads(p.read_text())
def gate_evals(results_dir: Path, thresholds: Path) -> list[str]:
floors = tomllib.loads(thresholds.read_text())["floors"]
seen = {f"{e['suite']}.{e['metric']}": e["value"] for e in load(results_dir, "evals.jsonl", [])}
return [f"{k}: {seen.get(k, 'missing')} < {v}" for k, v in floors.items() if not (k in seen and seen[k] >= v)]
# script_id: day_090_capstone_r5_launch/scripts/launch_report
def fmt(x, spec: str = ".3f") -> str:
return "n/a" if x is None or (isinstance(x, float) and math.isnan(x)) else format(x, spec)
def max_sustained(load_steps: list[dict]) -> dict:
"""The highest step with no 5xx, no own 429s and p95 within 1.5x of the lightest step: left of the knee."""
base = load_steps[0]["p95_ms"] if load_steps else math.nan
good = [r for r in load_steps if r["error_rate"] == 0 and r["own_429_rate"] == 0 and r["p95_ms"] <= 1.5 * base]
return max(good, key=lambda r: r["rps_target"], default={})
def build_readme(results_dir: Path) -> str:
meta, evals = load(results_dir, "meta.json", {}), load(results_dir, "evals.jsonl", [])
slo, drill = load(results_dir, "slo.json", {"slos": [], "cost": {}}), load(results_dir, "drill.json", {})
top, fails = max_sustained(load(results_dir, "load_stage_b.json", [])), load(results_dir, "known_failures.json", [])
cost, volume = slo["cost"], meta.get("asks_per_day", {})
out = [f"## Atlas {meta.get('tag', 'r5.0.0')}: numbers first", "",
f"Measured {meta.get('date', time.strftime('%Y-%m-%d'))} on `{meta.get('model', 'gpt-6-luna')}`. "
"Method and raw tables: [LAUNCH_REPORT.md](LAUNCH_REPORT.md). Prices as of 2026-11; verify at the provider.",
"", "| Evals | Value | n | Dataset |", "|---|---|---|---|"]
out += [f"| {e['suite']}: {e['metric']} | {fmt(e['value'])} | {e['n']} | {e['dataset']} |" for e in evals]
out += ["", "| Cost (USD) | p50 | p95 | cache hit |", "|---|---|---|---|"]
out += [f"| $/request, {route} | {fmt(c['p50'], '.5f')} | {fmt(c['p95'], '.5f')} | {fmt(c['cache_hit'], '.0%')} |"
for route, c in cost.items()]
month = cost.get("ask", {}).get("mean", math.nan) * sum(volume.values()) * 30
out += [f"| Monthly ask projection, USD: {sum(volume.values()):,} asks/day, {len(volume)} tenants (stated volume) "
f"| {fmt(month, ',.2f')} | | |"]
# script_id: day_090_capstone_r5_launch/scripts/launch_report
# (build_readme continues: latency, capacity, reliability, SLOs, known failures, links)
out += ["", "| Latency, capacity, reliability | Value |", "|---|---|",
f"| ask p50 / p95, ms, at {fmt(top.get('rps_target'), 'g')} rps | "
f"{fmt(top.get('p50_ms'), '.0f')} / {fmt(top.get('p95_ms'), '.0f')} |",
f"| TTFT p95, ms (one-chunk stream: equals answer time) | {fmt(top.get('ttft_p95_ms'), '.0f')} |",
f"| investigate p95, ms (event log) | {fmt(cost.get('investigate', {}).get('p95_ms'), '.0f')} |",
f"| Max sustained load before 429s or the knee | {fmt(top.get('rps_target'), 'g')} rps, "
f"{fmt(top.get('tokens_per_min'), ',.0f')} TPM |",
f"| Chaos drill: availability / fallback share | "
f"{fmt(drill.get('availability'), '.2%')} / {fmt(drill.get('fallback_share'), '.0%')} |",
f"| Chaos drill: detect / mitigate / alerts clear | {fmt(drill.get('mttd_s'), 'g')} s / "
f"{fmt(drill.get('mitigated_s'), 'g')} s / {fmt(drill.get('mttr_s'), 'g')} s |"]
out += [f"| SLO {r['name']}: actual vs target (budget left) | {fmt(r['actual'], '.2%')} vs {r['target']:.1%} "
f"({fmt(r['budget_left'], '.0%')}) |" for r in slo["slos"]]
top3 = sorted(fails, key=lambda f: -f["count"])[:3]
out += ["", "**Known failures** (top 3 by count):"]
out += [f"{i}. {f['failure']}: {f['count']} cases ({f['issue']})" for i, f in enumerate(top3, 1)]
out += ["", f"**Demo:** {meta.get('demo_url', '<demo url>')} · 2-minute video: {meta.get('video_url', '<video>')}",
"", f"**Tags:** `{meta.get('tag', 'r5.0.0')}` is this release. `r5-reference` is the course's reference "
"solution (R1-R5 complete); `git checkout r5-reference` to compare, or to continue from it on Day 91."]
return "\n".join(out)
# script_id: day_090_capstone_r5_launch/scripts/launch_report
SAMPLE = { # SAMPLE DATA, illustrative shapes only: run the real scripts for your numbers
"meta.json": {"tag": "r5.0.0", "date": "SAMPLE (not a measurement)", "demo_url": "<demo url>",
"asks_per_day": {"acme": 2000, "zephyr": 1000, "orbit-maintainers": 500}},
"evals.jsonl": [{"suite": "retrieval", "metric": "recall_at_5", "value": 0.9, "n": 40,
"dataset": "retrieval_queries@v1"}],
"load_stage_b.json": [{"rps_target": 1, "p50_ms": 1500.0, "p95_ms": 3000.0, "ttft_p95_ms": 3100.0,
"error_rate": 0.0, "own_429_rate": 0.0, "tokens_per_min": 200000}],
"drill.json": {"availability": 1.0, "fallback_share": 0.5, "mttd_s": 30, "mitigated_s": 90, "mttr_s": 330},
"slo.json": {"slos": [{"name": "availability", "target": 0.995, "actual": 0.999, "budget_left": 0.8}],
"cost": {"ask": {"mean": 0.0004, "p50": 0.0005, "p95": 0.0009, "cache_hit": 0.3}}},
"known_failures.json": [{"failure": "cites a tenant runbook the online check doesn't know", "count": 7,
"issue": "#12"}],
}
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("results", nargs="?", default="results")
ap.add_argument("--gate", help="eval_thresholds.toml: exit 1 if any floor is missed")
ap.add_argument("--sample", action="store_true")
a = ap.parse_args()
if a.gate:
problems = gate_evals(Path(a.results), Path(a.gate))
print("\n".join(problems) or "all eval floors met")
sys.exit(1 if problems else 0)
d = Path(tempfile.mkdtemp()) if a.sample else Path(a.results)
for name, data in (SAMPLE.items() if a.sample else []):
lines = [json.dumps(x) for x in data] if name.endswith(".jsonl") else [json.dumps(data)]
(d / name).write_text("\n".join(lines))
print(build_readme(d))
--sample output (illustrative numbers, not measured):
## Atlas r5.0.0: numbers first
Measured SAMPLE (not a measurement) on `gpt-6-luna`. Method and raw tables: [LAUNCH_REPORT.md](LAUNCH_REPORT.md). Prices as of 2026-11; verify at the provider.
| Evals | Value | n | Dataset |
|---|---|---|---|
| retrieval: recall_at_5 | 0.900 | 40 | retrieval_queries@v1 |
| Cost (USD) | p50 | p95 | cache hit |
|---|---|---|---|
| $/request, ask | 0.00050 | 0.00090 | 30% |
| Monthly ask projection, USD: 3,500 asks/day, 3 tenants (stated volume) | 42.00 | | |
| Latency, capacity, reliability | Value |
|---|---|
| ask p50 / p95, ms, at 1 rps | 1500 / 3000 |
| TTFT p95, ms (one-chunk stream: equals answer time) | 3100 |
| investigate p95, ms (event log) | n/a |
| Max sustained load before 429s or the knee | 1 rps, 200,000 TPM |
| Chaos drill: availability / fallback share | 100.00% / 50% |
| Chaos drill: detect / mitigate / alerts clear | 30 s / 90 s / 330 s |
| SLO availability: actual vs target (budget left) | 99.90% vs 99.5% (80%) |
**Known failures** (top 3 by count):
1. cites a tenant runbook the online check doesn't know: 7 cases (#12)
**Demo:** <demo url> · 2-minute video: <video>
**Tags:** `r5.0.0` is this release. `r5-reference` is the course's reference solution (R1-R5 complete); `git checkout r5-reference` to compare, or to continue from it on Day 91.
Known failures start from Day 89's list plus today's drill gaps and missed SLOs, each with a count and an issue link. A README with none reads as untested.
The LAUNCH_REPORT.md template:
<!-- file: LAUNCH_REPORT.md -->
# Atlas r5.0.0 launch report (<date>, <git sha>)
## Method
- Load: open-loop Poisson arrivals, scripts/load_test.py, generator on <machine>, steps <list>, 60 s each.
- Stage A (stub): <table>. Bottleneck found: <what>, fix: <commit>, before/after: <table>.
- Stage B (gpt-6-luna, cap $0.50): <table>; TPM observed vs tpm_needed(): <values>.
## Chaos drill (staging, <date>)
- Fault: <models> down for 5 min at 1 rps. Availability <x>, fallback <y>, degraded <z>.
- Timeline: <rep.timeline>. Postmortem: <link> (Day 88 template). Gaps filed: <issues>.
## SLOs (window <days>)
<slo table: target, actual, budget left, n>
## Cost
<cost_per_request by route and by model; cache savings; monthly projection with stated volumes>
## Evals at release
<evals.jsonl table with dataset versions and n; floors from eval_thresholds.toml>
## Known failures
<each: description, count, source (D44 taxonomy / online checks / drill), issue link>
Once the gate promotes, tag it: git tag -a r5.0.0 -m "R5: Atlas in production", then push. The course's reference solution is the r5-reference tag (R1–R5 with today's files; ⚠ the repo link ships with the course materials). Diff against it, or git checkout r5-reference to start Phase 10 from known-good code.
What Launch Taught You
| Choice | R5 picks | Pick the other when |
|---|---|---|
| One instance, in-process cache | simple, measured | you need replicas: Redis cache, Postgres event log |
| Canary by API key | stable per caller | many users share a key: bucket by user id |
| Fallback keeps availability | users get answers | cost or quality beats uptime: degrade (Exercise 3) |
Multi-service scale is the stop line; Exercise 4 is a first step.
In production
- Review SLOs and error budgets weekly; spent budget puts reliability ahead of features.
- Re-run the drill after any change to fallbacks or alerts.
- Put Day 88's model shutdown dates next to the SLO review.
- Re-run Stage B before a 2× traffic jump, and ramp rather than jump.
- Rotate the demo key on a schedule, and at once if its spend alert fires.
These numbers answer Day 97's "how would you scale it?", the drill is a STAR story for Day 98, and R1–R5 anchor Day 99's portfolio. Day 93 reuses load_test.py.
Go Deeper (optional)
Go deeper: the Stage A bottleneck hunt
Look for synchronous work on the event loop. Day 89's code has three candidates: KeyStore.lookup (a SQLite read in auth), deps.events.record (a SQLite write in finish()), and the semantic cache's linear scan over every stored entry. Here is the event-log write moved to a thread, still shielded from cancellation:
t.ev.status, t.ev.latency_ms = status, round((time.perf_counter() - t.started) * 1000, 1)
- deps.events.record(t.ev) # metrics only: no question or answer text
+ with anyio.CancelScope(shield=True): # a cancelled request still gets its row
+ await asyncio.to_thread(deps.events.record, t.ev) # Stage A fix: SQLite off the event loop
Re-run the same steps and fill both rows from your runs; your numbers will differ by machine. If 50 requests/s is still flat, you have headroom far beyond what the provider will sell you, and that's the finding.
| Stage A (stub) | 5 rps | 10 rps | 20 rps | 50 rps |
|---|---|---|---|---|
| p95 before the fix (ms) | ||||
| p95 after the fix (ms) | ||||
| 5xx / own 429 | ||||
| API CPU / generator CPU |
Go deeper: coordinated omission, measured
Point both kinds of generator at the same overloaded toy server. The closed loop has 8 virtual users, each waiting for its reply. The open loop sends 120 requests/s on schedule against an 80/s capacity.
# script_id: day_090_capstone_r5_launch/closed_vs_open
# Coordinated omission, measured: one overloaded toy server, a closed-loop and an open-loop generator.
# Needs scripts/load_test.py (httpx2 + fastapi). No key, no services.
import asyncio
import random
import time
import httpx2
from scripts.load_test import Spend, pct, run_step, shoot, toy_api
async def closed_loop(client: httpx2.AsyncClient, users: int, seconds: float) -> list[float]:
"""Each virtual user waits for its reply before sending again, and times from its own send."""
lat, stop = [], time.perf_counter() + seconds
async def user() -> None:
while time.perf_counter() < stop:
s = await shoot(client, "/v1/ask", {"question": "q"}, "k", time.perf_counter())
lat.append(s.latency_ms)
await asyncio.gather(*(user() for _ in range(users)))
return lat
async def main() -> None:
async with httpx2.AsyncClient(base_url="http://toy", transport=httpx2.ASGITransport(app=toy_api())) as c:
lat = await closed_loop(c, users=8, seconds=3)
print(f"closed loop: {len(lat) / 3:.0f} requests/s got through, p95 {pct(lat, 95):.0f} ms")
r = await run_step(c, [("/v1/ask", {"question": "q"}, "k")] * 1_000, 120, 3, Spend(), 99.0,
random.Random(0))
print(f"open loop at 120/s: {r.rps_achieved:.0f} requests/s got through, p95 {r.p95_ms:.0f} ms")
asyncio.run(main())
closed loop: 79 requests/s got through, p95 105 ms
open loop at 120/s: 77 requests/s got through, p95 1453 ms
Both generators hit the same throughput ceiling. The closed loop reports a p95 of about two service times because it stopped sending while the server was busy; the open loop shows the queue real users would sit in. Gil Tene named this blind spot coordinated omission in his talks on latency measurement. Tools such as wrk2 and k6's arrival-rate executors are built to avoid it.
Go deeper: burn-rate alerts on SLOs
Day 86's alerts fire on thresholds ("fallback share > 20% over 5 minutes"). SLO-based alerting asks a different question: at this rate, when will the error budget run out? The burn rate is the observed bad share divided by the allowed bad share. With a 99.5% target the allowed share is 0.5%, so a 5% error rate is a burn rate of 10: a 30-day budget gone in 3 days.
The pattern from Google's SRE workbook pairs a fast and a slow window. Page when the burn rate exceeds about 14 over both the last hour and the last 5 minutes: that spends about 2% of a 30-day budget in an hour, and the short window makes the alert stop soon after recovery. Open a ticket when the rate exceeds about 1 over 3 days. With slo_report() this is two calls with different windows and one comparison; window_days accepts fractions if you change its type to float. Check the multipliers against the workbook's "Alerting on SLOs" chapter before adopting them; they assume a 30-day window. The book and workbook are linked from the SRE book table of contents.
Go deeper: reading your provider headroom during a test
LoadReport.provider_429_rate sees pushback only after Atlas absorbed it. To see the ceiling coming, read the provider's own counters: every Responses API reply carries x-ratelimit-remaining-tokens and x-ratelimit-reset-tokens (plus request and project variants), documented in the rate-limits guide. Day 84's ratelimit_headers block shows how to read them with client.responses.with_raw_response.create(...). During Stage B, add a sidecar that sends one tiny request every 10 s with the same project key and logs remaining / limit. When that ratio drops below about 10% at a load step, you've found the provider knee before any user got a 429. The guide also warns that a sudden rise in traffic can earn slow_down 429s even within your limits, which is one more reason Stage B ramps in steps.
Common Pitfalls
- A calm p95 under overload. A closed-loop generator sends less exactly when the server is slow. Schedule arrivals open-loop and time from the scheduled send.
- The generator saturates first. On the API's box, you measure your laptop. Run it elsewhere and watch its CPU;
rps_achievedwell below target at low load is the tell. own429near 100%, blamed on the provider. It's your limiter ({"error": "rate_limited"}+Retry-After). Keep the three columns apart and load-test on aninternal-plan key.- A real-model test burns the budget. Always pass
--max-usdwith headroom (it counts finished requests), on a project key with a spend limit. - SLO targets copied from blog posts. Derive them from Stage B plus headroom, treat
nanas no data, and revisit after a week. - The demo key leaks into the UI repo or the browser. Keep it in platform secrets, the tenant ask-only, and prove the quota before posting the link.
Key Takeaways
- A launch is proven by numbers. Start on paper: tokens per request × rps × 60, minus cache hits, against your TPM limit, with the limiter at about 80%.
- Generate load open-loop and time from the scheduled send. Hunt your own bottlenecks keylessly (Stage A), then take a small, capped look at the real model (Stage B).
- A chaos drill measures what users saw (availability, fallback and degraded shares) and what you did (detect, mitigate, recover). In R5 an outage shows up as fallback share and cost, not errors.
- SLOs are targets for request-level SLIs with an error budget, set from measurements plus headroom. Quality and cost are SLOs too, and no data is not 100%.
- Ship from a tag through full evals, an approval, a smoke test and a canary gate, with a rollback you've run once, and lead the README with numbers and known failures.
Quick Reference
Capstone R5, Part 2, on one card
| API / pattern | Use it for | Gotcha |
|---|---|---|
tpm_needed(in, out, rps, cache_hit_rate) | capacity before any test | read your real limit |
load_test(url, keys=, rps_steps=, max_usd=) | Stage A and B | internal-plan key, open loop |
own_429_rate / provider_429_rate / error_rate | reading a step | three owners; never sum them |
ChaosClient, set_chaos() | staging drills | never in prod; finally clears it |
run_chaos_drill(url, keys=, minutes=5) | outage rehearsal | run where the event log lives |
slo_report(log, days), cost_per_request(log) | weekly review | nan = no SLI data |
on: push: tags + environment: production | prod releases | merges never ship |
release_gate + canary_gate() | after a prod deploy | needs many keys |
Exercises
Warm-up: read the tails. Add a p99 column to the load table, flag steps where p99 > 3 × p95, and print one line counting the arrivals the cost cap aborted.
Peek at the approach
LoadReportalready carriesp99_msandaborted; the table only has to show them. A p99 far above p95 means a few requests hit a wall (a cold connection, a retry, a fallback) that the p95 hides.# script_id: day_090_capstone_r5_launch/solution_ex1 # Load table v2: a p99 column, a tail flag and an aborted-by-cost-cap line. Needs scripts/load_test.py. from scripts.load_test import LoadReport def print_table_v2(reports: list[LoadReport]) -> None: print(f"{'rps':>5} {'n':>5} {'p50':>6} {'p95':>6} {'p99':>6} {'5xx':>5} {'own429':>6} {'$':>7}") for r in reports: tail = " <- p99 > 3x p95" if r.p99_ms > 3 * r.p95_ms else "" print(f"{r.rps_target:5g} {r.n:5} {r.p50_ms:6.0f} {r.p95_ms:6.0f} {r.p99_ms:6.0f} " f"{r.error_rate:5.1%} {r.own_429_rate:6.1%} {r.cost_usd:7.4f}{tail}") aborted, sent = sum(r.aborted for r in reports), sum(r.n for r in reports) if aborted: print(f"cost cap: {aborted} of {aborted + sent} arrivals aborted ({aborted / (aborted + sent):.0%})") if __name__ == "__main__": # toy reports, not measurements print_table_v2([LoadReport(1, 1.0, 60, 850, 1900, 2400, 1200, 0, 0, 0, 230_000, 0.03), LoadReport(2, 1.9, 104, 900, 2600, 9100, 1500, 0, 0.02, 0, 450_000, 0.06, aborted=16)])rps n p50 p95 p99 5xx own429 $ 1 60 850 1900 2400 0.0% 0.0% 0.0300 2 104 900 2600 9100 0.0% 2.0% 0.0600 <- p99 > 3x p95 cost cap: 16 of 180 arrivals aborted (9%)Core: caching as capacity. Re-run Stage B with the semantic cache off and on, and report the TPM, p95 and $/request deltas per step.
Peek at the approach
Add a
NoCachewith the same surface as Day 85'sSemanticCache, and select it inbuild_depswithcache=NoCache() if os.environ.get("ATLAS_CACHE") == "off" else SemanticCache(...). Run Stage B twice with different--outfiles, then compare. Expect TPM and $/request to drop with the cache on, roughly by the hit rate, and p95 to move little, because the slow requests are the misses.# script_id: day_090_capstone_r5_launch/solution_ex2 # Cache off vs on: a NoCache drop-in for build_deps, and a delta table for two saved load runs. # Run: python solution_ex2.py results/load_cache_on.json results/load_cache_off.json import json import sys from pathlib import Path class NoCache: """Day 85's SemanticCache surface that never hits: ATLAS_CACHE=off in build_deps.""" def lookup(self, scope: str, query: str) -> None: return None def store(self, scope: str, query: str, answer: object) -> None: pass def invalidate(self, prefix: str) -> int: return 0 def deltas(on: list[dict], off: list[dict]) -> None: per = lambda r: r["cost_usd"] / r["n"] if r["n"] else float("nan") print(f"{'rps':>5} {'TPM on':>9} {'TPM off':>9} {'p95 on':>7} {'p95 off':>7} {'$/req on':>9} {'$/req off':>9}") for a, b in zip(on, off): print(f"{a['rps_target']:5g} {a['tokens_per_min']:9,.0f} {b['tokens_per_min']:9,.0f} {a['p95_ms']:7.0f} " f"{b['p95_ms']:7.0f} {per(a):9.5f} {per(b):9.5f}") if __name__ == "__main__": if len(sys.argv) == 3: deltas(*(json.loads(Path(p).read_text()) for p in sys.argv[1:])) else: print("usage: solution_ex2.py <load_cache_on.json> <load_cache_off.json>")Core: reliability vs quality. Run the chaos drill twice, once with fallback models available and once degraded-only (every model down), and compare availability, degraded share and citation rate.
Peek at the approach
Degraded-only means taking down every model in the pools, not only the primary, because Day 89's
build_depsalways addsSTRONGto them. Availability stays at about 100% in both runs. What changes is what users got: real answers from the fallback, or cited links only. Day 86's online checks score only model answers, so read the degraded share as the quality signal. Wait between runs so the 5-minute alert windows drain.# script_id: day_090_capstone_r5_launch/solution_ex3 # Fallback vs degraded-only: the same drill twice. Run on the staging API host, like chaos_drill. # Env: as scripts/chaos_drill.py. Cost: two drills of ~420 requests each on the reference backend. import json import os import time from pathlib import Path from atlas.monitoring import EventLog, metrics from scripts.chaos_drill import run_chaos_drill if __name__ == "__main__": keys, base = json.loads(os.environ["ATLAS_DRILL_KEYS"]), os.environ.get("ATLAS_API_URL", "http://localhost:8000") log = EventLog(Path(os.environ.get("ATLAS_STATE_DIR", ".atlas")) / "events.db") primary = os.environ.get("ATLAS_MODEL", "gpt-6-luna") every = {primary, *os.environ.get("ATLAS_FALLBACK_MODELS", "gpt-6.1-terra").split(","), os.environ.get("ATLAS_STRONG_MODEL", "gpt-6.1-terra")} - {""} print(f"{'mode':14} {'avail':>7} {'fallback':>8} {'degraded':>8} {'cited':>6}") for label, down in [("fallback", {primary}), ("degraded-only", every)]: rep = run_chaos_drill(base, keys=keys, minutes=5, down=down, log=log) m = metrics(log.window(7)) # the drill's 7 minutes print(f"{label:14} {rep.availability:7.1%} {rep.fallback_share:8.0%} {rep.degraded_share:8.0%} " f"{m['citation_rate']:6.0%}") time.sleep(300) # let the alert windows drain before the next runStretch (optional): two replicas. Run two API replicas behind your platform's load balancer, or two compose services on different ports, and show that the rate limit and the kill switch still hold across both.
Peek at the approach
Quotas and kill switches live in Redis, so they're shared. The semantic cache and the SQLite event log are per replica: the hit rate drops, and SLO reports need both logs or a shared Postgres log (Day 86 notes the swap). This script proves the two shared guards.
# script_id: day_090_capstone_r5_launch/solution_ex4 # Two replicas, one set of limits. Needs both replicas running and two tenant keys in the env. # Run: ATLAS_ACME_KEY=... ATLAS_ZEPHYR_KEY=... python solution_ex4.py http://localhost:8001 http://localhost:8002 import json import os import sys import urllib.error import urllib.request def ask(base: str, key: str, q: str = "How do retries back off?") -> tuple[int, dict]: req = urllib.request.Request(f"{base}/v1/ask", json.dumps({"question": q}).encode(), {"Content-Type": "application/json", "Authorization": f"Bearer {key}"}) try: with urllib.request.urlopen(req, timeout=60) as r: return r.status, json.loads(r.read()) except urllib.error.HTTPError as e: return e.code, {} if __name__ == "__main__": a, b = (sys.argv[1:3] + ["http://localhost:8001", "http://localhost:8002"])[:2] codes = [ask((a, b)[i % 2], os.environ.get("ATLAS_ACME_KEY", ""))[0] for i in range(61)] assert codes[:60] == [200] * 60 and codes[60] == 429, f"limit not shared: {codes.count(429)} x 429" print("rate limit shared: request 61 got 429 from the other replica") input("run `python -m atlas.ops fallback force --reason ex4`, wait 5 s, press Enter ") key = os.environ.get("ATLAS_ZEPHYR_KEY", "") models = {ask(u, key, q)[1].get("model") for u in (a, b) for q in ("What is a lease?", "What is a flow?")} assert models == {"gpt-6.1-terra"}, f"kill switch not shared: {models}" print("kill switch shared: both replicas answer on the fallback model")Run Day 88's CLI as
python -m atlas.ops(R5 keeps it inatlas/ops.py) withREDIS_URLset to the API'sATLAS_REDIS_URL. Undo the lever afterwards withpython -m atlas.ops fallback auto --reason ex4-done.
References & Further Reading
- OpenAI: Rate limits: RPM/TPM, usage tiers,
x-ratelimit-*headers. - OpenAI: Pricing: the prices behind every $/request here (as of 2026-11).
- GitHub: Managing environments: required reviewers and secrets for
production. - GitHub: Workflow syntax: tag filters,
needs,environment,if: failure(). - Google SRE book: the "Service Level Objectives" chapter; its workbook covers alerting on SLOs.
- Hugging Face: Spaces overview: hosting the demo UI and its Secrets.
- Gil Tene's talk "How NOT to Measure Latency": the classic on coordinated omission (search the title; hosting changes).