Module 4: Measuring Latency Honestly
Tool Latency as Fixed Data
Description
With the honesty problem already solved in lesson 03, this lesson fixes, once and for the rest of this module, the central artifact: TOOL_LATENCY_MS. You already used it, in passing, in lessons 02 and 03 — this is the lesson that precisely stops on it: where its four numbers come from, why book_room is, by a wide margin, the most expensive tool, and how it becomes observability/latency_model.py's first block, the artifact this module delivers in lesson 08.
Connection to the module
This lesson corresponds to this guide's second operations-layer artifact: observability/run_logger.py (Module 2) and observability/cost_calculator.py (Module 3) already exist; observability/latency_model.py starts here, with its first piece. Lessons 05 through 08 build on top of this dictionary without declaring it again — exactly as Module 3 fixed claude-sonnet-5's pricing once in its lesson 04 and reused it unchanged for the rest of the guide.
Analogy: a repair shop's fixed rate
A repair shop that quotes in advance doesn't tell the customer "let's see how long it takes, and we'll bill you by the clock afterward" — it gives a fixed rate per type of job: an oil change, one hour; a tire alignment, half an hour; a full inspection, two hours. Those rates aren't the exact time the mechanic took this particular time — sometimes it's less, sometimes more — they're a declared commitment, meant to let the customer know what to expect before the job starts, and to let the shop plan its day without depending on exactly how long, to the second, each specific job takes.
TOOL_LATENCY_MS is exactly that fixed rate, applied to Reservo's four tools. It doesn't promise book_room is going to take exactly 120 milliseconds in a real system — lesson 03 already said so, bluntly — it declares a reference rate, meant so every exercise in this guide knows what to expect before running the code.
The dictionary, fixed once
TOOL_LATENCY_MS = {
"list_rooms": 40,
"get_quote": 25,
"book_room": 120,
"cancel_booking": 90,
}
Four tools, four numbers, in int milliseconds. This guide never changes any of the four again from this point until Module 8's close — it's the same discipline you already saw with claude-sonnet-5's pricing in Module 3: a constant gets fixed once, with a declared reason, and the rest of the guide cites it without questioning it again.
Why these four numbers, and not others
The order of these four values isn't arbitrary — it reflects a real intuition about what type of operation each tool is, the same intuition you already used to tell "read-only" tools apart from "write" tools in agent-fundamentals:
for name, latency_ms in sorted(TOOL_LATENCY_MS.items(), key=lambda item: item[1]):
kind = "lectura (sin efectos)" if name in ("list_rooms", "get_quote") else "escritura (efectos reales)"
print(f"{name:15} {latency_ms:4} ms -- {kind}")
What to expect:
get_quote 25 ms -- lectura (sin efectos)
list_rooms 40 ms -- lectura (sin efectos)
cancel_booking 90 ms -- escritura (efectos reales)
book_room 120 ms -- escritura (efectos reales)
Both read-only tools (get_quote, list_rooms) are the cheapest of the four; both write tools (cancel_booking, book_room) are the most expensive. get_quote is the cheapest of all because it's, literally, in-memory arithmetic — ROOM_RATE_CENTS[room] * hours — touching no shared state. book_room is the most expensive because, in a real system, creating a booking almost always means writing to a database — something that takes an order of magnitude longer than an in-memory calculation. This guide didn't measure that on a real system — that would be, again, the real clock lesson 03 ruled out — it declared it with that intuition, the same way any real system designer would declare their first estimates before having production data.
Looking up an individual tool call's latency
You already saw the simplest way to use this dictionary in lesson 02: a direct lookup.
print("latencia de get_quote :", TOOL_LATENCY_MS["get_quote"], "ms")
print("latencia de book_room :", TOOL_LATENCY_MS["book_room"], "ms")
print("latencia de una tool desconocida:", TOOL_LATENCY_MS.get("delete_everything", 0), "ms")
What to expect:
latencia de get_quote : 25 ms
latencia de book_room : 120 ms
latencia de una tool desconocida: 0 ms
Look at the third line: TOOL_LATENCY_MS.get("delete_everything", 0) instead of TOOL_LATENCY_MS["delete_everything"]. This guide uses .get(name, 0) in every function that queries this dictionary — never direct bracket access — for a concrete reason: a tool not in the dictionary (because it was never declared, or because there's a typo in its name) shouldn't bring down the entire latency calculation with a KeyError; it should, instead, explicitly contribute 0 ms, so the mistake shows up in the final report (a suspiciously low latency) instead of in an exception that halts the entire process.
The total with all four tools, once each
You already know this figure from Module 1, Exercise 3 of lesson 05 — it's worth confirming here, as the natural upper bound for a run that uses each tool exactly once:
total_todas = sum(TOOL_LATENCY_MS.values())
print("suma de las cuatro tools, una vez cada una:", total_todas, "ms")
What to expect:
suma de las cuatro tools, una vez cada una: 275 ms
275 ms — the same number you already calculated by hand in Module 1. This figure doesn't depend on the order the tools get called in (summing is commutative), only on which ones get called. Hold onto it: it's the natural ceiling for a "normal" Reservo run (one that doesn't repeat any tool), and it's going to serve as a reference point when lesson 06 calculates percentiles over a real batch — a run that gets close to 275 ms is, almost certainly, using all four tools in a single exchange.
Common mistakes
-
Declaring a different
TOOL_LATENCY_MSin every lesson, "to vary the example." No — this dictionary gets fixed once, here, and reused unchanged for the rest of the module (and the guide). Changing it would break every figure you already calculated in lessons 02 and 03. -
Using
TOOL_LATENCY_MS[name]with brackets instead of.get(name, 0). With Reservo's four canonical tools you'll never notice the difference — all four are always present — but as soon as a later lesson (or your own code) registers a new tool without also adding it to this dictionary, bracket access blows up with aKeyErrorinstead of gracefully degrading to0ms. -
Assuming the order of keys in the dictionary matters for anything. It doesn't — a Python
dict(since version 3.7) preserves insertion order for iteration, but no function in this module depends on what order the four tools appear in insideTOOL_LATENCY_MS. The worked example'ssorted(...)explicitly orders by value, precisely because insertion order isn't what matters here. -
Confusing "read tool" with "cheap tool" as a universal rule. In Reservo, the correlation holds — both read tools are, in fact, the cheapest — but it's a design decision for this guide, not a general law. A real system could have a slow read tool (a complex database query) and a fast write tool (a simple
INSERTinto a small table). -
Thinking
275ms is "the normal latency" of any run. It's the ceiling, not the average — most Reservo runs, as you already saw in Module 1 and you're going to confirm with lesson 06's batch, use fewer than all four tools, so their total latency is, almost always, well below275.
Exercises
Exercise 1: Calculate the latency of two consecutive get_quotes (Easy)
Without running anything: if a script calls get_quote twice (to compare two quotes, without booking anything), what's its total latency? Confirm with code.
See solution
50 ms — 25 + 25, because get_quote gets called twice and each call contributes its individual latency, regardless of it being "the same" tool repeated.
print("dos get_quote seguidas:", TOOL_LATENCY_MS["get_quote"] * 2, "ms")
Expected output:
dos get_quote seguidas: 50 ms
Explanation: latency is summed per call, not per unique tool — two calls to the same tool cost double what one costs, exactly the same as two calls to different tools summing together. There's no "discount" for repeating the same tool within a run.
Exercise 2: Find the tool that, added to a list_rooms + get_quote run, most increases total latency (Medium)
A run already has list_rooms + get_quote (65 ms). Without running anything first, decide which of the two remaining tools (book_room or cancel_booking) would add more latency if added to the run, and confirm with code.
See solution
book_room (120 ms) adds more than cancel_booking (90 ms) — the difference is 30 ms.
base = TOOL_LATENCY_MS["list_rooms"] + TOOL_LATENCY_MS["get_quote"]
con_book = base + TOOL_LATENCY_MS["book_room"]
con_cancel = base + TOOL_LATENCY_MS["cancel_booking"]
print("base (list_rooms + get_quote):", base, "ms")
print("+ book_room :", con_book, "ms")
print("+ cancel_booking :", con_cancel, "ms")
Expected output:
base (list_rooms + get_quote): 65 ms
+ book_room : 185 ms
+ cancel_booking : 155 ms
Explanation: 185 (with book_room) exceeds 155 (with cancel_booking) — and 185 is, in fact, the same figure from Ana's canonical run, which follows exactly this sequence. This confirms, with numbers, something you already intuited in the "Why these four numbers" section: book_room isn't just the most expensive individual tool — it's also the one with the most impact when added to a run that already had other tools.
Exercise 3: Design a fifth hypothetical tool and decide its latency, with a justification (Hard)
Reservo might need, in the future, a send_confirmation_email(booking_id) tool that sends a confirmation email after a booking. Without implementing it — this is a design exercise, not a code one — decide what modeled latency you'd assign it in TOOL_LATENCY_MS, and justify your choice by comparing it against the four existing tools.
See solution
A reasonable assignment: TOOL_LATENCY_MS["send_confirmation_email"] = 200. The justification: sending an email typically involves a network call to an external service (a transactional email provider) — an operation that crosses the network is, almost always, slower than a write to a local database like book_room (120 ms), because it adds round-trip network latency to the external provider's processing latency. A figure in the 150-250 ms range would be defensible; a figure below 120 (book_room's) would be hard to justify, because it would imply talking to an external service is faster than writing locally — the opposite of what happens in most real systems. This exercise has no single "correct" answer — unlike this guide's four canonical tools, whose values are fixed and cited — but it does have better- and worse-justified answers, and that justification (read vs. write, local vs. network) is exactly the same criterion you already used to understand why book_room is more expensive than get_quote.
Summary and next step
- We fixed
TOOL_LATENCY_MS—list_rooms=40,get_quote=25,book_room=120,cancel_booking=90— asobservability/latency_model.py's first block, reused unchanged for the rest of this module. - We confirmed, with real execution, why the order of these four values isn't arbitrary: both read-only tools are the cheapest; both write tools, the most expensive — the same distinction you already used in
agent-fundamentalsto design each tool's contracts. - We confirmed, again, that summing all four tools once each is
275ms — a "normal" Reservo run's natural ceiling, a reference point you're going to reuse in lesson 06. - With
TOOL_LATENCY_MSnow fixed, the next lesson builds this module's central function: a complete run's total latency, with the correct nuance about rejectedtool_uses.
Next lesson: 05 — Total Run Latency. We build total_run_latency_ms, run over Ana's canonical run, and confirm why a tool_use rejected on validation doesn't add a single millisecond to the total.
Additional resources
- Python — dictionaries, the
.get()method — The safe way to queryTOOL_LATENCY_MS, used in every function in this module from this lesson onward. - Anthropic — Building effective agents — On why an agent's write tools — the ones with real effects — tend to also be the most expensive to run on a real system.
- Python —
sorted()functions withkey— The technique used to orderTOOL_LATENCY_MS.items()by latency in the worked example. - Python 3.14 — What's New — The version every line of code in this lesson ran on.