AI Engineering (Applied) · Cost, Latency & Observability

Cost, Latency & Observability

A feature that works in a demo isn’t shipped — it’s demoed. Shipping means it stays fast enough that people don’t leave, cheap enough that the finance team doesn’t panic, and transparent enough that when something goes wrong you can see inside and fix it. This lesson is about the three dials every foundation-model app lives and dies by — money, speed, and observability — and how watching them turns a one-off launch into a system that keeps getting better.

☺ Explain it like I’m 10

Imagine you open a lemonade stand run by a genius helper. Every cup costs you a little (money), people hate waiting in line (speed), and if the lemonade comes out wrong you need to peek behind the counter to find out why (seeing inside). Keep all three in check and your stand stays open — and gets a little better every day.

🦫Your host for this topic: 🦫 Benny the Beaver — Benny is the builder who cares less about clever demos and more about the boring plumbing that keeps a feature alive in production: bills, timers, and pipes you can inspect.

The token economy — what you pay for, and the clocks that matter

☺ Like you’re 10: The helper charges you by the word — both the words you say to it and the words it says back. And it doesn’t answer all at once: there’s a pause before the first word appears, then words come out one at a time like a printer. Two different clocks to watch.

Every foundation-model API bills the same basic way: you pay per token, where a token is a chunk of text (roughly ¾ of a word). There are two prices, and they’re usually different:

Your cost per request is simply (input_tokens × input_price) + (output_tokens × output_price). That means the two biggest levers on cost are how much you stuff into the prompt (the whole point of context engineering) and how long the answer runs. Neither is free, and both are under your control.

Speed has two clocks too, and confusing them is a classic mistake:

You pay per input and output token, and you’re racing two clocks — TTFT (the wait) and tokens/sec (the flow). Every optimization in this lesson moves one of these three numbers.
Input tokens prompt · history · docs 💲 input price Model reads ⏱ the wait → TTFT Output tokens stream out · tokens/sec 💲 output price (higher) t = 0 ◀── TTFT (user sees a spinner) ──▶ ◀ streaming ▶ Cost = (input tokens × input price) + (output tokens × output price) Latency the user feels ≈ TTFT + (output tokens ÷ tokens-per-second)

Caching — stop paying twice for the same thing

☺ Like you’re 10: If someone asks you the exact same question they asked five minutes ago, you don’t work it out again from scratch — you just repeat your saved answer. Caching is the app keeping a little notebook of "I’ve seen this before" so it can skip the expensive part.

Caching means reusing work you already did instead of paying for it again. In model apps there are three flavors, from strictest to loosest match:

◆ Key idea

Cache from strict to loose: try the exact cache, then a semantic match, and use prompt caching to discount the unavoidable prefix on everything else. Each layer catches requests the layer above missed.

The catch — and it’s a big one — is invalidation. A cache is a snapshot, and snapshots go stale. If your refund policy changes but the cache still holds the old answer, you’ll confidently serve last month’s policy. Every cache needs an expiry (a time-to-live) and a way to clear entries when the underlying facts change. A semantic cache adds a second risk: set the "close enough" threshold too loose and you’ll serve a saved answer to a question that only looked similar.

Model routing & cascades — right-sizing every request

☺ Like you’re 10: You don’t call the world’s top surgeon to put on a bandage. Most questions are easy, so send them to a small, cheap, fast helper — and only escalate to the big expensive genius when the little one is out of its depth.

Not every request needs your most powerful (and most expensive, slowest) model. Model routing sends each request to the right-sized model. The most common pattern is a cascade: try a small model first, and only escalate to a larger one when needed.

A related lever is batching: when the work isn’t interactive (nightly summaries, bulk classification, back-filling labels), send many requests together. Providers often offer a cheaper, higher-latency batch tier precisely because you’ve told them "I don’t need this in the next second." Trading latency you don’t need for money you’d rather keep is one of the best deals in the stack.

Request a user asks Small model cheap · fast Good enough? 🐢 check / eval gate Large model strong · pricier Answer to the user no → escalate yes → return
◆ Rule of thumb

Start on the smallest model that passes your evals, and let a cascade escalate the hard minority. Most teams over-estimate how many requests truly need the flagship model — measure, don’t assume.

Latency as UX — perceived speed and hiding the slow parts

☺ Like you’re 10: Waiting for a whole story feels forever; watching the words appear as they’re written feels quick, even if it takes the same total time. And if part of a job is slow and boring, do it in the back room so the customer up front never has to wait for it.

Here’s the trick that makes model apps bearable: latency you can see is latency you can hide. The single most effective UX move is streaming — showing tokens the instant they’re generated instead of waiting for the whole answer. Total time is unchanged, but perceived latency plummets because the user gets feedback immediately (this is why TTFT matters so much more than total duration for how fast something feels).

Beyond streaming, you shift slow work out of the user’s way:

The goal isn’t always to make the model faster — it’s to make the wait feel shorter. Stream what you can, and move everything that isn’t interactive off the user’s critical path.

Observability — tracing so you can see inside

☺ Like you’re 10: When a toy breaks, you can’t fix it if it’s a sealed black box. Observability is putting a little camera on every step inside, so when the answer comes out wrong you can rewind and see exactly which step went sideways — and how much each step cost and how long it took.

A real feature is rarely one model call. It’s a chain: build the prompt, retrieve documents, call the model, maybe call tools, maybe loop, then format the output. When something goes wrong — a bad answer, a huge bill, a slow response — you need to see which step was responsible. That’s what tracing gives you.

A trace is the full record of one request, broken into spans — one span per step. Each span carries telemetry: how long it took, how many tokens it used, what it cost, and its inputs and outputs. Add them up and the trace tells you the total cost and latency of the request and exactly where each went.

Trace: one request → each bar is a span · width = latency prompt build 8 ms · 0 tok retrieval 40 ms · embed cost model call 820 ms · 1,340 tok · 💲 biggest tool call 110 ms · 0 tok output stream Total: ~978 ms · 1,340 tok · $— per request → the model call owns the cost & the clock

Notice what the trace reveals at a glance: the model call dominates both the time and the cost, retrieval is cheap, and the tool call is a minor tax. Without tracing you’d be guessing which knob to turn; with it, you know exactly where to spend your optimization effort. Tracing is typically wired in with an SDK that wraps each step, or via an open standard so traces flow into whatever dashboard you already use.

# Pseudocode: wrap each step in a span; spans nest into one trace
with trace("answer_request") as t:
    with t.span("prompt_build") as s:
        prompt = build_prompt(user_msg, history)
    with t.span("retrieval") as s:
        docs = retriever.search(user_msg, k=4)
        s.record(cost=embed_cost, latency=s.elapsed)
    with t.span("model_call") as s:
        reply = model.generate(prompt + docs)      # streamed
        s.record(in_tokens=reply.in_tok,
                 out_tokens=reply.out_tok,
                 cost=token_cost(reply),
                 latency=s.elapsed)
    t.finish()   # trace now knows TOTAL cost, latency, tokens per request
🎬 At the AI Academy
🦫

Benny the Beaver: The support assistant works great in the demo — but the bill tripled overnight and users say it "feels slow." I have no idea why.

🐢

Timmy the Turtle: You’re flying blind, Benny. Turn on tracing. One span per step — prompt, retrieval, model, tools — each stamped with its tokens, cost, and latency. Then we look instead of guessing.

🐿️

Nutty the Squirrel: Traces are in. Whoa — retrieval is dumping 20 chunks into every prompt. That’s thousands of input tokens per call, on the flagship model, for questions a small model could answer.

🦫

Benny the Beaver: So I trim retrieval to the top 4, add prompt caching for the system prompt, and route the easy questions to a small model first. TTFT drops, the bill falls off a cliff.

🐢

Timmy the Turtle: And the requests where the small model stumbled? I flagged those from the traces — they’re going straight into the eval set so we can prove the next change actually helps.

🦥

Sol the Sloth: Same total thinking, way less waiting… and now you can see it. That’s the whole game.

The feedback loop — production traces feed the evals

☺ Like you’re 10: Every real question your app gets is a free practice question for next time. Save the tricky ones into your quiz pile, and each new version has to pass the quiz before it ships. That’s how it keeps getting better instead of secretly getting worse.

This is where the whole track closes into a circle. The traces you collect in production aren’t just for firefighting — they’re the richest source of real test cases you’ll ever have. The loop:

  1. Capture. Every request leaves a trace with its inputs, outputs, cost, and latency.
  2. Curate. The interesting ones — failures, low-confidence answers, thumbs-down feedback, surprising costs — get pulled out and added to your eval set.
  3. Guard. Now every prompt tweak, model swap, or routing change must pass those real cases before it ships. Regressions get caught before users feel them.
  4. Repeat. New production traffic surfaces new edge cases, which become new evals. The system compounds.

Alongside the eval loop you run ongoing monitoring and alerting: dashboards on cost-per-request, TTFT, error rates, and cache hit-rate, with alerts when any of them breaches a threshold (a sudden cost spike usually means a prompt got bloated or the cache stopped hitting). And you watch for drift — the slow, silent kind where nothing errors but quality quietly slides: users start asking different questions, a provider updates a model, your retrieved docs go stale. Drift doesn’t throw an exception; it just makes your feature worse a little at a time, which is exactly why the traces-to-evals loop is what catches it.

◆ Key idea

Observability and evals are two ends of one loop. Traces tell you what is happening in production; evals turn that into a test you must pass to change anything. Wire them together and your app improves on autopilot instead of degrading in the dark.

Pitfalls

☺ Like you’re 10: Three ways this goes wrong: running with your eyes shut (no cameras inside), keeping an answer past its expiry date (stale notebook), and being so clever about picking helpers that all the checking makes it slower than just answering.

The failures here are predictable, and all three come from optimizing without measuring:

The common thread: every one of these is invisible until you measure it, and obvious once you do. Cost, latency, and observability aren’t a final polish step — they’re how you keep a shipped feature both alive and improving. And that improvement runs through the loop we just built: production traces become evals, evals gate every change, and the feature that shipped back where this tier began, with evals, keeps getting better all the way through this one. The tier ends where it began — measuring, so you can improve. For the broader operational picture beyond a single feature, keep going in Production & Ops.

⚠ Optimize with numbers, not vibes

Never tune cost or latency by intuition. Add tracing first, look at where the tokens and milliseconds actually go, change one thing, and confirm the trace moved the number you meant to move. Guessing is how you make something slower and more expensive while feeling productive.

◆ In practice you’ll reach for…

Tracing/observability: OpenTelemetry’s GenAI conventions are the open standard; hosted options include LangSmith, Langfuse, Helicone, and Arize Phoenix. Model routing: LiteLLM or OpenRouter put one API over many providers with fallback. Caching: Redis or a semantic-cache layer (e.g. GPTCache). Verify current options before you commit.

🦫 Benny’s workshop · 5 min

Take any AI feature you use (a chat assistant, a "summarize this" button). Ask it one short question and one long, complex one. For each, eyeball the two clocks: how long until the first word appears (TTFT), and how fast the rest streams (tokens/sec). Then guess the cost driver — was it mostly a big input (a long document you pasted) or a long output (a rambling answer)? You’ve just done a one-request "trace" by hand, and spotted which knob you’d turn first.

🐢 Timmy’s checkpoint

(1) You pay per input and output token — name two levers that lower each, and which one is usually priced higher. (2) What’s the difference between TTFT and tokens/sec, and why does streaming improve perceived speed without changing total time? (3) Explain prompt caching vs semantic caching, and the one bug both share. (4) What is a span, what telemetry does it carry, and how do production traces feed back into your eval set to keep the feature improving?

Check your answers
  1. Cost levers & which is pricier: Input cost drops by stuffing less into the prompt — trim the system prompt, retrieve fewer docs, cache the prefix. Output cost drops by generating less — ask for shorter, less rambling answers, and route easy requests to a smaller model. Output tokens are usually priced higher than input, so a model that rambles for 800 tokens when 80 would do quietly doubles your bill.
  2. TTFT vs tokens/sec: TTFT (time-to-first-token) is how long the user waits before anything appears — the loading-spinner feeling; tokens/sec (throughput) is how fast text streams once it has started. Streaming shows each token the instant it’s generated, so the user gets feedback immediately: total time is unchanged, but perceived latency plummets because the wait before the first word — which dominates how fast it feels — is gone.
  3. Prompt vs semantic caching + shared bug: Prompt (prefix) caching marks a long, unchanging prefix — a big system prompt, tool definitions, a fixed document — so the model doesn’t re-process it on every call; you still generate a fresh answer, just cheaper and faster. Semantic caching matches on meaning via embeddings and reuses a prior answer when a new question is close enough. Both share the invalidation bug: a cache is a snapshot that goes stale, so once the underlying facts change it will confidently serve the old, wrong answer unless you set expiries (a TTL) and clear entries when sources change.
  4. Spans, telemetry & the feedback loop: A span is the record of one step in a request (prompt build, retrieval, model call, tool call, output), and it carries telemetry — how long it took, how many tokens it used, what it cost, and its inputs and outputs; adding up the spans gives one trace with the total cost and latency. Production traces feed the evals by a loop: capture every request’s trace, curate the interesting ones (failures, low-confidence answers, thumbs-down, surprising costs) into the eval set, then guard every prompt tweak, model swap, or routing change against those real cases so regressions and drift are caught before users feel them.